http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingContext.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingContext.java b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingContext.java new file mode 100644 index 0000000..2bf3e5b --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingContext.java @@ -0,0 +1,68 @@ +/*************************************************************************************************************************** + * 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; + +/** + * Configurable properties on the {@link UrlEncodingSerializer} and {@link UrlEncodingParser} classes. + * <p> + * Use the {@link UrlEncodingSerializer#setProperty(String, Object)} and + * {@link UrlEncodingParser#setProperty(String, Object)} methods to set property values. + * + * @author James Bognar ([email protected]) + */ +public final class UrlEncodingContext implements Cloneable { + + /** + * Serialize bean property collections/arrays as separate key/value pairs ({@link Boolean}, default=<jk>false</jk>). + * <p> + * If <jk>false</jk>, serializing the array <code>[1,2,3]</code> results in <code>?key=$a(1,2,3)</code>. + * If <jk>true</jk>, serializing the same array results in <code>?key=1&key=2&key=3</code>. + * <p> + * Example: + * <p class='bcode'> + * <jk>public class</jk> A { + * <jk>public</jk> String[] f1 = {<js>"a"</js>,<js>"b"</js>}; + * <jk>public</jk> List<String> f2 = <jk>new</jk> LinkedList<String>(Arrays.<jsm>asList</jsm>(<jk>new</jk> String[]{<js>"c"</js>,<js>"d"</js>})); + * } + * + * UrlEncodingSerializer s1 = <jk>new</jk> UrlEncodingParser(); + * UrlEncodingSerializer s2 = <jk>new</jk> UrlEncodingParser().setProperty(UrlEncodingContext.<jsf>URLENC_expandedParams</jsf>, <jk>true</jk>); + * + * String s1 = p1.serialize(<jk>new</jk> A()); <jc>// Produces "f1=(a,b)&f2=(c,d)"</jc> + * String s2 = p2.serialize(<jk>new</jk> A()); <jc>// Produces "f1=a&f1=b&f2=c&f2=d"</jc> + * </p> + * <p> + * <b>Important note:</b> If parsing multi-part parameters, it's highly recommended to use Collections or Lists + * as bean property types instead of arrays since arrays have to be recreated from scratch every time a value + * is added to it. + * <p> + * This option only applies to beans. + */ + public static final String URLENC_expandedParams = "UrlEncoding.expandedParams"; + + boolean + expandedParams = false; + + //-------------------------------------------------------------------------------- + // Overridden methods + //-------------------------------------------------------------------------------- + + @Override /* Cloneable */ + public UrlEncodingContext clone() { + try { + return (UrlEncodingContext)super.clone(); + } 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/UrlEncodingParser.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingParser.java b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingParser.java new file mode 100644 index 0000000..d759965 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingParser.java @@ -0,0 +1,554 @@ +/*************************************************************************************************************************** + * 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 URL-encoded text into POJO models. + * + * + * <h6 class='topic'>Media types</h6> + * <p> + * Handles <code>Content-Type</code> types: <code>application/x-www-form-urlencoded</code> + * + * + * <h6 class='topic'>Description</h6> + * <p> + * Parses URL-Encoded text (e.g. <js>"foo=bar&baz=bing"</js>) into POJOs. + * <p> + * Expects parameter values to be in UON notation. + * <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 BeanContext} + * </ul> + * + * + * @author James Bognar ([email protected]) + */ +@SuppressWarnings({ "rawtypes", "unchecked", "hiding" }) +@Consumes("application/x-www-form-urlencoded") +public class UrlEncodingParser extends UonParser { + + /** Reusable instance of {@link UrlEncodingParser}. */ + public static final UrlEncodingParser DEFAULT = new UrlEncodingParser().lock(); + + /** Reusable instance of {@link UrlEncodingParser}. */ + public static final UrlEncodingParser DEFAULT_WS_AWARE = new UrlEncodingParser().setProperty(UON_whitespaceAware, true).lock(); + + /** + * Constructor. + */ + public UrlEncodingParser() { + setProperty(UON_decodeChars, true); + } + + private <T> T parseAnything(UrlEncodingParserSession session, ClassMeta<T> nt, ParserReader r, Object outer) 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(); + + int c = r.peek(); + if (c == '?') + r.read(); + + Object o; + + if (ft.isObject()) { + ObjectMap m = new ObjectMap(bc); + parseIntoMap(session, r, m, bc.string(), bc.object()); + o = m.cast(); + } else if (ft.isMap()) { + Map m = (ft.canCreateNewInstance() ? (Map)ft.newInstance() : new ObjectMap(bc)); + o = parseIntoMap(session, r, m, ft.getKeyType(), ft.getValueType()); + } 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 { + // It could be a non-bean with _class attribute. + ObjectMap m = new ObjectMap(bc); + ClassMeta<Object> valueType = object(); + parseIntoMap(session, r, m, string(), valueType); + if (m.containsKey("_class")) + o = m.cast(); + else if (m.containsKey("_value")) + o = session.getBeanContext().convertToType(m.get("_value"), ft); + else if (ft.isCollection()) { + // ?1=foo&2=bar... + Collection c2 = ft.canCreateNewInstance() ? (Collection)ft.newInstance() : new ObjectList(bc); + Map<Integer,Object> t = new TreeMap<Integer,Object>(); + for (Map.Entry<String,Object> e : m.entrySet()) { + String k = e.getKey(); + if (StringUtils.isNumeric(k)) + t.put(Integer.valueOf(k), bc.convertToType(e.getValue(), ft.getElementType())); + } + c2.addAll(t.values()); + o = c2; + } else { + if (ft.getNotABeanReason() != null) + throw new ParseException(session, "Class ''{0}'' could not be instantiated as application/x-www-form-urlencoded. Reason: ''{1}''", ft, ft.getNotABeanReason()); + throw new ParseException(session, "Malformed application/x-www-form-urlencoded input for class ''{0}''.", ft); + } + } + + 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.peek(); + if (c == -1) + return m; + + final int S1=1; // Looking for attrName start. + final int S2=2; // Found attrName end, looking for =. + final int S3=3; // Found =, looking for valStart. + final int S4=4; // Looking for & or end. + boolean isInEscape = false; + + int state = S1; + K currAttr = null; + while (c != -1) { + c = r.read(); + if (! isInEscape) { + if (state == S1) { + if (c == -1) + return m; + r.unread(); + Object attr = parseAttr(session, r, true); + currAttr = session.trim(session.getBeanContext().convertToType(attr, keyType)); + state = S2; + c = 0; // Avoid isInEscape if c was '\' + } else if (state == S2) { + if (c == '\u0002') + state = S3; + else if (c == -1 || c == '\u0001') { + m.put(currAttr, null); + if (c == -1) + return m; + state = S1; + } + } else if (state == S3) { + if (c == -1 || c == '\u0001') { + V value = convertAttrToType(session, m, "", valueType); + m.put(currAttr, value); + if (c == -1) + return m; + state = S1; + } else { + // For performance, we bypass parseAnything for string values. + V value = (V)(valueType.isString() ? super.parseString(session, r.unread(), true) : super.parseAnything(session, valueType, r.unread(), m, true)); + + // If we already encountered this parameter, turn it into a list. + if (m.containsKey(currAttr) && valueType.isObject()) { + Object v2 = m.get(currAttr); + if (! (v2 instanceof ObjectList)) { + v2 = new ObjectList(v2); + m.put(currAttr, (V)v2); + } + ((ObjectList)v2).add(value); + } else { + m.put(currAttr, value); + } + state = S4; + c = 0; // Avoid isInEscape if c was '\' + } + } else if (state == S4) { + if (c == '\u0001') + state = S1; + else if (c == -1) { + return m; + } + } + } + isInEscape = (c == '\\' && ! isInEscape); + } + if (state == S1) + throw new ParseException(session, "Could not find attribute name on object."); + if (state == S2) + throw new ParseException(session, "Could not find '=' following attribute name on object."); + if (state == S3) + throw new ParseException(session, "Dangling '=' found in object entry"); + if (state == S4) + throw new ParseException(session, "Could not find end of object."); + + return null; // Unreachable. + } + + private <T> BeanMap<T> parseIntoBeanMap(UrlEncodingParserSession session, ParserReader r, BeanMap<T> m) throws Exception { + + int c = r.peek(); + if (c == -1) + return m; + + final int S1=1; // Looking for attrName start. + final int S2=2; // Found attrName end, looking for =. + final int S3=3; // Found =, looking for valStart. + final int S4=4; // Looking for , or } + boolean isInEscape = false; + + int state = S1; + String currAttr = ""; + int currAttrLine = -1, currAttrCol = -1; + while (c != -1) { + c = r.read(); + if (! isInEscape) { + if (state == S1) { + if (c == -1) { + return m; + } + r.unread(); + currAttrLine= r.getLine(); + currAttrCol = r.getColumn(); + currAttr = parseAttrName(session, r, true); + if (currAttr == null) // Value was '%00' + return null; + state = S2; + } else if (state == S2) { + if (c == '\u0002') + state = S3; + else if (c == -1 || c == '\u0001') { + m.put(currAttr, null); + if (c == -1) + return m; + state = S1; + } + } else if (state == S3) { + if (c == -1 || c == '\u0001') { + if (! currAttr.equals("_class")) { + BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr); + if (pMeta == null) { + if (m.getMeta().isSubTyped()) { + m.put(currAttr, ""); + } else { + onUnknownProperty(session, currAttr, m, currAttrLine, currAttrCol); + } + } else { + session.setCurrentProperty(pMeta); + // In cases of "&foo=", create an empty instance of the value if createable. + // Otherwise, leave it null. + ClassMeta<?> cm = pMeta.getClassMeta(); + if (cm.canCreateNewInstance()) + pMeta.set(m, cm.newInstance()); + session.setCurrentProperty(null); + } + } + if (c == -1) + 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), true); + m.put(currAttr, value); + } else { + onUnknownProperty(session, currAttr, m, currAttrLine, currAttrCol); + parseAnything(session, object(), r.unread(), m.getBean(false), true); // Read content anyway to ignore it + } + } else { + session.setCurrentProperty(pMeta); + if (session.shouldUseExpandedParams(pMeta)) { + ClassMeta et = pMeta.getClassMeta().getElementType(); + Object value = parseAnything(session, et, r.unread(), m.getBean(false), true); + setName(et, value, currAttr); + pMeta.add(m, value); + } else { + ClassMeta<?> cm = pMeta.getClassMeta(); + Object value = parseAnything(session, cm, r.unread(), m.getBean(false), true); + setName(cm, value, currAttr); + pMeta.set(m, value); + } + session.setCurrentProperty(null); + } + } + state = S4; + } + } else if (state == S4) { + if (c == '\u0001') + state = S1; + else if (c == -1) { + return m; + } + } + } + isInEscape = (c == '\\' && ! isInEscape); + } + if (state == S1) + throw new ParseException(session, "Could not find attribute name on object."); + if (state == S2) + throw new ParseException(session, "Could not find '=' following attribute name on object."); + if (state == S3) + throw new ParseException(session, "Could not find value following '=' on object."); + if (state == S4) + throw new ParseException(session, "Could not find end of object."); + + return null; // Unreachable. + } + + /** + * Parse a URL query string into a simple map of key/value pairs. + * + * @param qs The query string to parse. + * @return A sorted {@link TreeMap} of query string entries. + * @throws Exception + */ + public Map<String,String[]> parseIntoSimpleMap(String qs) throws Exception { + + Map<String,String[]> m = new TreeMap<String,String[]>(); + + if (StringUtils.isEmpty(qs)) + return m; + + UonReader r = new UonReader(qs, true); + + final int S1=1; // Looking for attrName start. + final int S2=2; // Found attrName start, looking for = or & or end. + final int S3=3; // Found =, looking for valStart. + final int S4=4; // Found valStart, looking for & or end. + + try { + int c = r.peek(); + if (c == '?') + r.read(); + + int state = S1; + String currAttr = null; + while (c != -1) { + c = r.read(); + if (state == S1) { + if (c != -1) { + r.unread(); + r.mark(); + state = S2; + } + } else if (state == S2) { + if (c == -1) { + add(m, r.getMarked(), null); + } else if (c == '\u0001') { + m.put(r.getMarked(0,-1), null); + state = S1; + } else if (c == '\u0002') { + currAttr = r.getMarked(0,-1); + state = S3; + } + } else if (state == S3) { + if (c == -1 || c == '\u0001') { + add(m, currAttr, ""); + } else { + if (c == '\u0002') + r.replace('='); + r.unread(); + r.mark(); + state = S4; + } + } else if (state == S4) { + if (c == -1) { + add(m, currAttr, r.getMarked()); + } else if (c == '\u0001') { + add(m, currAttr, r.getMarked(0,-1)); + state = S1; + } else if (c == '\u0002') { + r.replace('='); + } + } + } + } finally { + r.close(); + } + + return m; + } + + private static void add(Map<String,String[]> m, String key, String val) { + boolean b = m.containsKey(key); + if (val == null) { + if (! b) + m.put(key, null); + } else if (b && m.get(key) != null) { + m.put(key, ArrayUtils.append(m.get(key), val)); + } else { + m.put(key, new String[]{val}); + } + } + + private Object[] parseArgs(UrlEncodingParserSession session, ParserReader r, ClassMeta<?>[] argTypes) throws Exception { + // TODO - This can be made more efficient. + BeanContext bc = session.getBeanContext(); + ClassMeta<TreeMap<Integer,String>> cm = bc.getMapClassMeta(TreeMap.class, Integer.class, String.class); + TreeMap<Integer,String> m = parseAnything(session, cm, r, session.getOuter()); + Object[] vals = m.values().toArray(new Object[m.size()]); + if (vals.length != argTypes.length) + throw new ParseException(session, "Argument lengths don't match. vals={0}, argTypes={1}", vals.length, argTypes.length); + for (int i = 0; i < vals.length; i++) { + String s = String.valueOf(vals[i]); + vals[i] = super.parseAnything(session, argTypes[i], new UonReader(s, false), session.getOuter(), true); + } + + return vals; + } + + /** + * Parses a single query parameter value into the specified class type. + * + * @param in The input query string value. + * @param type The class type of the object to create. + * @return A new instance of the specified type. + * @throws ParseException + */ + public <T> T parseParameter(CharSequence in, ClassMeta<T> type) throws ParseException { + if (in == null) + return null; + UonParserSession session = createParameterContext(in); + try { + UonReader r = session.getReader(); + return super.parseAnything(session, type, r, null, true); + } catch (ParseException e) { + throw e; + } catch (Exception e) { + throw new ParseException(session, e); + } finally { + session.close(); + } + } + + /** + * Parses a single query parameter value into the specified class type. + * + * @param in The input query string value. + * @param type The class type of the object to create. + * @return A new instance of the specified type. + * @throws ParseException + */ + public <T> T parseParameter(CharSequence in, Class<T> type) throws ParseException { + return parseParameter(in, getBeanContext().getClassMeta(type)); + } + + //-------------------------------------------------------------------------------- + // Overridden methods + //-------------------------------------------------------------------------------- + + @Override /* Parser */ + public UrlEncodingParserSession createSession(Object input, ObjectMap properties, Method javaMethod, Object outer) { + return new UrlEncodingParserSession(getContext(UrlEncodingParserContext.class), getBeanContext(), input, properties, javaMethod, outer); + } + + @Override /* Parser */ + protected <T> T doParse(ParserSession session, ClassMeta<T> type) throws Exception { + UrlEncodingParserSession s = (UrlEncodingParserSession)session; + type = s.getBeanContext().normalizeClassMeta(type); + UonReader r = s.getReader(); + T o = parseAnything(s, type, r, s.getOuter()); + return o; + } + + @Override /* ReaderParser */ + protected Object[] doParseArgs(ParserSession session, ClassMeta<?>[] argTypes) throws Exception { + UrlEncodingParserSession uctx = (UrlEncodingParserSession)session; + UonReader r = uctx.getReader(); + Object[] a = parseArgs(uctx, r, argTypes); + return a; + } + + @Override /* ReaderParser */ + protected <K,V> Map<K,V> doParseIntoMap(ParserSession session, Map<K,V> m, Type keyType, Type valueType) throws Exception { + UrlEncodingParserSession s = (UrlEncodingParserSession)session; + UonReader r = s.getReader(); + if (r.peek() == '?') + r.read(); + m = parseIntoMap(s, r, m, s.getBeanContext().getClassMeta(keyType), s.getBeanContext().getClassMeta(valueType)); + return m; + } + + @Override /* Parser */ + public UrlEncodingParser setProperty(String property, Object value) throws LockedException { + super.setProperty(property, value); + return this; + } + + @Override /* CoreApi */ + public UrlEncodingParser setProperties(ObjectMap properties) throws LockedException { + super.setProperties(properties); + return this; + } + + @Override /* CoreApi */ + public UrlEncodingParser addNotBeanClasses(Class<?>...classes) throws LockedException { + super.addNotBeanClasses(classes); + return this; + } + + @Override /* CoreApi */ + public UrlEncodingParser addTransforms(Class<?>...classes) throws LockedException { + super.addTransforms(classes); + return this; + } + + @Override /* CoreApi */ + public <T> UrlEncodingParser addImplClass(Class<T> interfaceClass, Class<? extends T> implClass) throws LockedException { + super.addImplClass(interfaceClass, implClass); + return this; + } + + @Override /* CoreApi */ + public UrlEncodingParser setClassLoader(ClassLoader classLoader) throws LockedException { + super.setClassLoader(classLoader); + return this; + } + + @Override /* Lockable */ + public UrlEncodingParser lock() { + super.lock(); + return this; + } + + @Override /* Lockable */ + public UrlEncodingParser clone() { + UrlEncodingParser c = (UrlEncodingParser)super.clone(); + return c; + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingParserContext.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingParserContext.java b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingParserContext.java new file mode 100644 index 0000000..20a0e2a --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingParserContext.java @@ -0,0 +1,81 @@ +/*************************************************************************************************************************** + * 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.*; + +/** + * Configurable properties on the {@link UrlEncodingParser} 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 UrlEncodingParser#setProperty(String,Object)} + * <li>{@link UrlEncodingParser#setProperties(ObjectMap)} + * <li>{@link UrlEncodingParser#addNotBeanClasses(Class[])} + * <li>{@link UrlEncodingParser#addTransforms(Class[])} + * <li>{@link UrlEncodingParser#addImplClass(Class,Class)} + * </ul> + * <p> + * See {@link ContextFactory} for more information about context properties. + * + * @author James Bognar ([email protected]) + */ +public class UrlEncodingParserContext extends UonParserContext { + + /** + * Serialize bean property collections/arrays as separate key/value pairs ({@link Boolean}, default=<jk>false</jk>). + * <p> + * If <jk>false</jk>, serializing the array <code>[1,2,3]</code> results in <code>?key=$a(1,2,3)</code>. + * If <jk>true</jk>, serializing the same array results in <code>?key=1&key=2&key=3</code>. + * <p> + * Example: + * <p class='bcode'> + * <jk>public class</jk> A { + * <jk>public</jk> String[] f1 = {<js>"a"</js>,<js>"b"</js>}; + * <jk>public</jk> List<String> f2 = <jk>new</jk> LinkedList<String>(Arrays.<jsm>asList</jsm>(<jk>new</jk> String[]{<js>"c"</js>,<js>"d"</js>})); + * } + * + * UrlEncodingSerializer s1 = <jk>new</jk> UrlEncodingParser(); + * UrlEncodingSerializer s2 = <jk>new</jk> UrlEncodingParser().setProperty(UrlEncodingContext.<jsf>URLENC_expandedParams</jsf>, <jk>true</jk>); + * + * String s1 = p1.serialize(<jk>new</jk> A()); <jc>// Produces "f1=(a,b)&f2=(c,d)"</jc> + * String s2 = p2.serialize(<jk>new</jk> A()); <jc>// Produces "f1=a&f1=b&f2=c&f2=d"</jc> + * </p> + * <p> + * <b>Important note:</b> If parsing multi-part parameters, it's highly recommended to use Collections or Lists + * as bean property types instead of arrays since arrays have to be recreated from scratch every time a value + * is added to it. + * <p> + * This option only applies to beans. + */ + public static final String URLENC_expandedParams = "UrlEncoding.expandedParams"; + + + final boolean + expandedParams; + + /** + * Constructor. + * <p> + * Typically only called from {@link ContextFactory#getContext(Class)}. + * + * @param cf The factory that created this context. + */ + public UrlEncodingParserContext(ContextFactory cf) { + super(cf); + this.expandedParams = cf.getProperty(URLENC_expandedParams, boolean.class, false); + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingParserSession.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingParserSession.java b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingParserSession.java new file mode 100644 index 0000000..d5a9177 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingParserSession.java @@ -0,0 +1,77 @@ +/*************************************************************************************************************************** + * 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.UrlEncodingParserContext.*; + +import java.io.*; +import java.lang.reflect.*; + +import org.apache.juneau.*; + +/** + * Session object that lives for the duration of a single use of {@link UrlEncodingParser}. + * <p> + * This class is NOT thread safe. It is meant to be discarded after one-time use. + * + * @author James Bognar ([email protected]) + */ +public class UrlEncodingParserSession extends UonParserSession { + + private final boolean expandedParams; + + /** + * 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 UrlEncodingParserSession(UrlEncodingParserContext ctx, BeanContext beanContext, Object input, ObjectMap op, Method javaMethod, Object outer) { + super(ctx, beanContext, input, op, javaMethod, outer); + if (op == null || op.isEmpty()) { + expandedParams = ctx.expandedParams; + } else { + expandedParams = op.getBoolean(URLENC_expandedParams, false); + } + } + + /** + * Returns <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs. + * + * @param pMeta The metadata on the bean property. + * @return <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs. + */ + public final boolean shouldUseExpandedParams(BeanPropertyMeta<?> pMeta) { + ClassMeta<?> cm = pMeta.getClassMeta(); + if (cm.isArray() || cm.isCollection()) { + if (expandedParams) + return true; + if (pMeta.getBeanMeta().getClassMeta().getUrlEncodingMeta().isExpandedParams()) + return true; + } + return false; + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingSerializer.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingSerializer.java b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingSerializer.java new file mode 100644 index 0000000..fd04afb --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingSerializer.java @@ -0,0 +1,463 @@ +/*************************************************************************************************************************** + * 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 static org.apache.juneau.urlencoding.UrlEncodingSerializerContext.*; + +import java.io.*; +import java.lang.reflect.*; +import java.net.*; +import java.util.*; + +import org.apache.juneau.*; +import org.apache.juneau.annotation.*; +import org.apache.juneau.internal.*; +import org.apache.juneau.serializer.*; +import org.apache.juneau.transform.*; + +/** + * Serializes POJO models to URL-encoded notation with UON-encoded values (a notation for URL-encoded query paramter values). + * + * + * <h6 class='topic'>Media types</h6> + * <p> + * Handles <code>Accept</code> types: <code>application/x-www-form-urlencoded</code> + * <p> + * Produces <code>Content-Type</code> types: <code>application/x-www-form-urlencoded</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 + * URL-encoded notation would be as follows: + * </p> + * <p class='bcode'> + * <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 "a=b&c=$n(1)&d=$b(false)&e=$a(f,$n(1),$b(false))&g=$o(h=i)"</jc> + * String s = UrlEncodingSerializer.<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 = UrlEncodingSerializer.<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 "name=John+Doe&age=23&address=$o(street=123+Main+St,city=Anywhere,state=NY,zip=$n(12345))&deceased=$b(false)"</jc> + * String s = UrlEncodingSerializer.<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 = UrlEncodingSerializer.<jsf>DEFAULT_SIMPLE</jsf>.serialize(s); + * </p> + * + * @author James Bognar ([email protected]) + */ +@Produces("application/x-www-form-urlencoded") +@SuppressWarnings("hiding") +public class UrlEncodingSerializer extends UonSerializer { + + /** Reusable instance of {@link UrlEncodingSerializer}, all default settings. */ + public static final UrlEncodingSerializer DEFAULT = new UrlEncodingSerializer().lock(); + + /** Reusable instance of {@link UrlEncodingSerializer.Simple}. */ + public static final UrlEncodingSerializer DEFAULT_SIMPLE = new Simple().lock(); + + /** Reusable instance of {@link UrlEncodingSerializer.SimpleExpanded}. */ + public static final UrlEncodingSerializer DEFAULT_SIMPLE_EXPANDED = new SimpleExpanded().lock(); + + /** Reusable instance of {@link UrlEncodingSerializer.Readable}. */ + public static final UrlEncodingSerializer DEFAULT_READABLE = new Readable().lock(); + + /** + * Constructor. + */ + public UrlEncodingSerializer() { + setProperty(UON_encodeChars, true); + } + + /** + * Equivalent to <code><jk>new</jk> UrlEncodingSerializer().setProperty(UonSerializerContext.<jsf>UON_simpleMode</jsf>,<jk>true</jk>);</code>. + */ + @Produces(value={"application/x-www-form-urlencoded-simple"},contentType="application/x-www-form-urlencoded") + public static class Simple extends UrlEncodingSerializer { + /** Constructor */ + public Simple() { + setProperty(UON_simpleMode, true); + } + } + + /** + * Equivalent to <code><jk>new</jk> UrlEncodingSerializer().setProperty(UonSerializerContext.<jsf>UON_simpleMode</jsf>,<jk>true</jk>).setProperty(UonSerializerContext.<jsf>URLENC_expandedParams</jsf>,<jk>true</jk>);</code>. + */ + @Produces(value={"application/x-www-form-urlencoded-simple"},contentType="application/x-www-form-urlencoded") + public static class SimpleExpanded extends Simple { + /** Constructor */ + public SimpleExpanded() { + setProperty(URLENC_expandedParams, true); + } + } + + /** + * Equivalent to <code><jk>new</jk> UrlEncodingSerializer().setProperty(UonSerializerContext.<jsf>UON_useWhitespace</jsf>,<jk>true</jk>);</code>. + */ + public static class Readable extends UrlEncodingSerializer { + /** Constructor */ + public Readable() { + setProperty(UON_useWhitespace, true); + } + } + + /** + * Workhorse method. Determines the type of object, and then calls the + * appropriate type-specific serialization method. + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + private SerializerWriter serializeAnything(UrlEncodingSerializerSession session, UonWriter out, Object o) throws Exception { + BeanContext bc = session.getBeanContext(); + + boolean addClassAttr; // Add "_class" attribute to element? + ClassMeta<?> aType; // The actual type + ClassMeta<?> gType; // The generic type + + aType = session.push("root", o, object()); + session.indent--; + if (aType == null) + aType = object(); + + gType = aType.getTransformedClassMeta(); + addClassAttr = (session.isAddClassAttrs()); + + // 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); + } + + if (gType.isMap()) { + if (o instanceof BeanMap) + serializeBeanMap(session, out, (BeanMap)o, addClassAttr); + else + serializeMap(session, out, (Map)o, gType); + } else if (gType.hasToObjectMapMethod()) { + serializeMap(session, out, gType.toObjectMap(o), gType); + } else if (gType.isBean()) { + serializeBeanMap(session, out, bc.forBean(o), addClassAttr); + } else if (gType.isCollection()) { + serializeMap(session, out, getCollectionMap((Collection)o), bc.getMapClassMeta(Map.class, Integer.class, gType.getElementType())); + } else { + // All other types can't be serialized as key/value pairs, so we create a + // mock key/value pair with a "_value" key. + out.append("_value="); + super.serializeAnything(session, out, o, null, null, null, false, true); + } + + session.pop(); + return out; + } + + private Map<Integer,Object> getCollectionMap(Collection<?> c) { + Map<Integer,Object> m = new TreeMap<Integer,Object>(); + int i = 0; + for (Object o : c) + m.put(i++, o); + return m; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private SerializerWriter serializeMap(UrlEncodingSerializerSession session, UonWriter out, Map m, ClassMeta<?> type) throws Exception { + + m = session.sort(m); + + ClassMeta<?> keyType = type.getKeyType(), valueType = type.getValueType(); + + int depth = session.getIndent(); + boolean addAmp = false; + + 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); + + + if (session.shouldUseExpandedParams(value)) { + Iterator i = value instanceof Collection ? ((Collection)value).iterator() : ArrayUtils.iterator(value); + while (i.hasNext()) { + if (addAmp) + out.cr(depth).append('&'); + out.appendObject(key, false, true, true).append('='); + super.serializeAnything(session, out, i.next(), null, (key == null ? null : key.toString()), null, false, true); + addAmp = true; + } + } else { + if (addAmp) + out.cr(depth).append('&'); + out.appendObject(key, false, true, true).append('='); + super.serializeAnything(session, out, value, valueType, (key == null ? null : key.toString()), null, false, true); + addAmp = true; + } + } + + return out; + } + + @SuppressWarnings({ "rawtypes" }) + private SerializerWriter serializeBeanMap(UrlEncodingSerializerSession session, UonWriter out, BeanMap<?> m, boolean addClassAttr) throws Exception { + int depth = session.getIndent(); + + boolean addAmp = 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 (value != null && session.shouldUseExpandedParams(pMeta)) { + ClassMeta cm = pMeta.getClassMeta(); + // Transformed object array bean properties may be transformed resulting in ArrayLists, + // so we need to check type if we think it's an array. + Iterator i = (cm.isCollection() || value instanceof Collection) ? ((Collection)value).iterator() : ArrayUtils.iterator(value); + while (i.hasNext()) { + if (addAmp) + out.cr(depth).append('&'); + + out.appendObject(key, false, true, true).append('='); + + super.serializeAnything(session, out, i.next(), pMeta.getClassMeta().getElementType(), key, pMeta, false, true); + + addAmp = true; + } + } else { + if (addAmp) + out.cr(depth).append('&'); + + out.appendObject(key, false, true, true).append('='); + + super.serializeAnything(session, out, value, pMeta.getClassMeta(), key, pMeta, false, true); + + addAmp = true; + } + + } + return out; + } + + //-------------------------------------------------------------------------------- + // Methods for constructing individual parameter values. + //-------------------------------------------------------------------------------- + + /** + * Converts the specified object to a string using this serializers {@link BeanContext#convertToType(Object, Class)} method + * and runs {@link URLEncoder#encode(String,String)} against the results. + * Useful for constructing URL parts. + * + * @param o The object to serialize. + * @return The serialized object. + */ + public String serializeUrlPart(Object o) { + try { + // Shortcut for simple types. + ClassMeta<?> cm = getBeanContext().getClassMetaForObject(o); + if (cm != null) + if (cm.isCharSequence() || cm.isNumber() || cm.isBoolean()) + return o.toString(); + + StringWriter w = new StringWriter(); + UonSerializerSession s = createSession(w, null, null); + super.doSerialize(s, o); + return w.toString(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + //-------------------------------------------------------------------------------- + // Overridden methods + //-------------------------------------------------------------------------------- + + @Override /* Serializer */ + public UrlEncodingSerializerSession createSession(Object output, ObjectMap properties, Method javaMethod) { + return new UrlEncodingSerializerSession(getContext(UrlEncodingSerializerContext.class), getBeanContext(), output, properties, javaMethod); + } + + @Override /* Serializer */ + protected void doSerialize(SerializerSession session, Object o) throws Exception { + UrlEncodingSerializerSession s = (UrlEncodingSerializerSession)session; + serializeAnything(s, s.getWriter(), o); + } + + @Override /* CoreApi */ + public UrlEncodingSerializer setProperty(String property, Object value) throws LockedException { + super.setProperty(property, value); + return this; + } + + @Override /* CoreApi */ + public UrlEncodingSerializer setProperties(ObjectMap properties) throws LockedException { + super.setProperties(properties); + return this; + } + + @Override /* CoreApi */ + public UrlEncodingSerializer addNotBeanClasses(Class<?>...classes) throws LockedException { + super.addNotBeanClasses(classes); + return this; + } + + @Override /* CoreApi */ + public UrlEncodingSerializer addTransforms(Class<?>...classes) throws LockedException { + super.addTransforms(classes); + return this; + } + + @Override /* CoreApi */ + public <T> UrlEncodingSerializer addImplClass(Class<T> interfaceClass, Class<? extends T> implClass) throws LockedException { + super.addImplClass(interfaceClass, implClass); + return this; + } + + @Override /* CoreApi */ + public UrlEncodingSerializer setClassLoader(ClassLoader classLoader) throws LockedException { + super.setClassLoader(classLoader); + return this; + } + + @Override /* Lockable */ + public UrlEncodingSerializer lock() { + super.lock(); + return this; + } + + @Override /* Lockable */ + public UrlEncodingSerializer clone() { + UrlEncodingSerializer c = (UrlEncodingSerializer)super.clone(); + return c; + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingSerializerContext.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingSerializerContext.java b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingSerializerContext.java new file mode 100644 index 0000000..0ba2c09 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingSerializerContext.java @@ -0,0 +1,81 @@ +/*************************************************************************************************************************** + * 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.*; + +/** + * Configurable properties on the {@link UrlEncodingSerializer} 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 UrlEncodingSerializer#setProperty(String,Object)} + * <li>{@link UrlEncodingSerializer#setProperties(ObjectMap)} + * <li>{@link UrlEncodingSerializer#addNotBeanClasses(Class[])} + * <li>{@link UrlEncodingSerializer#addTransforms(Class[])} + * <li>{@link UrlEncodingSerializer#addImplClass(Class,Class)} + * </ul> + * <p> + * See {@link ContextFactory} for more information about context properties. + * + * @author James Bognar ([email protected]) + */ +public class UrlEncodingSerializerContext extends UonSerializerContext { + + /** + * Serialize bean property collections/arrays as separate key/value pairs ({@link Boolean}, default=<jk>false</jk>). + * <p> + * If <jk>false</jk>, serializing the array <code>[1,2,3]</code> results in <code>?key=$a(1,2,3)</code>. + * If <jk>true</jk>, serializing the same array results in <code>?key=1&key=2&key=3</code>. + * <p> + * Example: + * <p class='bcode'> + * <jk>public class</jk> A { + * <jk>public</jk> String[] f1 = {<js>"a"</js>,<js>"b"</js>}; + * <jk>public</jk> List<String> f2 = <jk>new</jk> LinkedList<String>(Arrays.<jsm>asList</jsm>(<jk>new</jk> String[]{<js>"c"</js>,<js>"d"</js>})); + * } + * + * UrlEncodingSerializer s1 = <jk>new</jk> UrlEncodingParser(); + * UrlEncodingSerializer s2 = <jk>new</jk> UrlEncodingParser().setProperty(UrlEncodingContext.<jsf>URLENC_expandedParams</jsf>, <jk>true</jk>); + * + * String s1 = p1.serialize(<jk>new</jk> A()); <jc>// Produces "f1=(a,b)&f2=(c,d)"</jc> + * String s2 = p2.serialize(<jk>new</jk> A()); <jc>// Produces "f1=a&f1=b&f2=c&f2=d"</jc> + * </p> + * <p> + * <b>Important note:</b> If parsing multi-part parameters, it's highly recommended to use Collections or Lists + * as bean property types instead of arrays since arrays have to be recreated from scratch every time a value + * is added to it. + * <p> + * This option only applies to beans. + */ + public static final String URLENC_expandedParams = "UrlEncoding.expandedParams"; + + + final boolean + expandedParams; + + /** + * Constructor. + * <p> + * Typically only called from {@link ContextFactory#getContext(Class)}. + * + * @param cf The factory that created this context. + */ + public UrlEncodingSerializerContext(ContextFactory cf) { + super(cf); + this.expandedParams = cf.getProperty(URLENC_expandedParams, boolean.class, false); + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingSerializerSession.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingSerializerSession.java b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingSerializerSession.java new file mode 100644 index 0000000..11e5a9e --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingSerializerSession.java @@ -0,0 +1,86 @@ +/*************************************************************************************************************************** + * 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.UrlEncodingParserContext.*; + +import java.lang.reflect.*; + +import org.apache.juneau.*; +import org.apache.juneau.json.*; + +/** + * Session object that lives for the duration of a single use of {@link UrlEncodingSerializer}. + * <p> + * This class is NOT thread safe. It is meant to be discarded after one-time use. + * + * @author James Bognar ([email protected]) + */ +public class UrlEncodingSerializerSession extends UonSerializerSession { + + private final boolean expandedParams; + + /** + * 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. + */ + public UrlEncodingSerializerSession(UrlEncodingSerializerContext ctx, BeanContext beanContext, Object output, ObjectMap op, Method javaMethod) { + super(ctx, beanContext, output, op, javaMethod); + if (op == null || op.isEmpty()) { + expandedParams = ctx.expandedParams; + } else { + expandedParams = op.getBoolean(URLENC_expandedParams, false); + } + } + + /** + * Returns <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs. + * + * @param pMeta The metadata on the bean property. + * @return <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs. + */ + public final boolean shouldUseExpandedParams(BeanPropertyMeta<?> pMeta) { + ClassMeta<?> cm = pMeta.getClassMeta(); + if (cm.isArray() || cm.isCollection()) { + if (expandedParams) + return true; + if (pMeta.getBeanMeta().getClassMeta().getUrlEncodingMeta().isExpandedParams()) + return true; + } + return false; + } + + /** + * Returns <jk>true</jk> if the specified value should be represented as an expanded parameter list. + * + * @param value The value to check. + * @return <jk>true</jk> if the specified value should be represented as an expanded parameter list. + */ + public final boolean shouldUseExpandedParams(Object value) { + if (value == null || ! expandedParams) + return false; + ClassMeta<?> cm = getBeanContext().getClassMetaForObject(value).getTransformedClassMeta(); + if (cm.isArray() || cm.isCollection()) { + if (expandedParams) + return true; + } + return false; + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/urlencoding/annotation/UrlEncoding.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/urlencoding/annotation/UrlEncoding.java b/juneau-core/src/main/java/org/apache/juneau/urlencoding/annotation/UrlEncoding.java new file mode 100644 index 0000000..66dd263 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/urlencoding/annotation/UrlEncoding.java @@ -0,0 +1,41 @@ +/*************************************************************************************************************************** + * 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.annotation; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.*; + +import java.lang.annotation.*; + +import org.apache.juneau.urlencoding.*; + +/** + * Annotation that can be applied to classes, fields, and methods to tweak how + * they are handled by {@link UrlEncodingSerializer} and {@link UrlEncodingParser}. + * + * @author James Bognar ([email protected]) + */ +@Documented +@Target({TYPE}) +@Retention(RUNTIME) +@Inherited +public @interface UrlEncoding { + + /** + * When true, bean properties of type array or Collection will be expanded into multiple key=value pairings. + * <p> + * This annotation is identical in behavior to using the {@link UrlEncodingContext#URLENC_expandedParams} + * property, but applies to only instances of this bean. + */ + boolean expandedParams() default false; +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/urlencoding/annotation/package.html ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/urlencoding/annotation/package.html b/juneau-core/src/main/java/org/apache/juneau/urlencoding/annotation/package.html new file mode 100644 index 0000000..6391eae --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/urlencoding/annotation/package.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<!-- +/*************************************************************************************************************************** + * 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. + * + ***************************************************************************************************************************/ + --> +<html> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> + <style type="text/css"> + /* For viewing in Page Designer */ + @IMPORT url("../../../../../../../javadoc.css"); + + /* For viewing in REST interface */ + @IMPORT url("../htdocs/javadoc.css"); + body { + margin: 20px; + } + </style> + <script> + /* Replace all @code and @link tags. */ + window.onload = function() { + document.body.innerHTML = document.body.innerHTML.replace(/\{\@code ([^\}]+)\}/g, '<code>$1</code>'); + document.body.innerHTML = document.body.innerHTML.replace(/\{\@link (([^\}]+)\.)?([^\.\}]+)\}/g, '<code>$3</code>'); + } + </script> +</head> +<body> +<p>URL-Encoding annotations</p> +</body> +</html> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/urlencoding/doc-files/Example_HTML.png ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/urlencoding/doc-files/Example_HTML.png b/juneau-core/src/main/java/org/apache/juneau/urlencoding/doc-files/Example_HTML.png new file mode 100644 index 0000000..ab74763 Binary files /dev/null and b/juneau-core/src/main/java/org/apache/juneau/urlencoding/doc-files/Example_HTML.png differ http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/urlencoding/doc-files/Example_UrlEncoding.png ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/urlencoding/doc-files/Example_UrlEncoding.png b/juneau-core/src/main/java/org/apache/juneau/urlencoding/doc-files/Example_UrlEncoding.png new file mode 100644 index 0000000..34de8a7 Binary files /dev/null and b/juneau-core/src/main/java/org/apache/juneau/urlencoding/doc-files/Example_UrlEncoding.png differ http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/urlencoding/doc-files/rfc_uon.txt ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/urlencoding/doc-files/rfc_uon.txt b/juneau-core/src/main/java/org/apache/juneau/urlencoding/doc-files/rfc_uon.txt new file mode 100644 index 0000000..c79c9c5 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/urlencoding/doc-files/rfc_uon.txt @@ -0,0 +1,352 @@ +Network Working Group J. Bognar +Request for Comments: 9999 C. Chaney +Category: Informational IBM + Jan 2014 + + ***DRAFT*** + URI Object Notation (UON): Generic Syntax + + +About this document + + This memo provides information for the Internet community. It does + not specify an Internet standard of any kind. Distribution of this + memo is unlimited. + +Copyright Notice + + Copyright (C) IBM Corp. 2014. All Rights Reserved. + +Abstract + + This document describes a grammar that builds upon RFC2396 + (Uniform Resource Identifiers). Its purpose is to define a + generalized object notation for URI query parameter values similar in + concept to Javascript Object Notation (RFC4627). The goal is a + syntax such that any data structure defined in JSON can be losslessly + defined in an equivalent URI-based grammar, yet be fully compliant + with the RFC2396 specification. + + This grammar provides the ability to construct the following data + structures in URL parameter values: + + OBJECT + ARRAY + NUMBER + BOOLEAN + STRING + NULL + + Example: + + The following shows a sample object defined in Javascript: + + var x = { + id: 1, + name: 'John Smith', + uri: 'http://sample/addressBook/person/1', + addressBookUri: 'http://sample/addressBook', + birthDate: '1946-08-12T00:00:00Z', + otherIds: null, + addresses: [ + { + uri: 'http://sample/addressBook/address/1', + personUri: 'http://sample/addressBook/person/1', + id: 1, + street: '100 Main Street', + city: 'Anywhereville', + state: 'NY', + zip: 12345, + isCurrent: true, + } + ] + } + + Using the "strict" syntax defined in this document, the equivalent + UON notation would be as follows: + + x=$o(id=$n(1),name=John+Smith,uri=http://sample/ + addressBook/person/1,addressBookUri=http://sample/ + addressBook,birthDate=1946-08-12T00:00:00Z,otherIds=%00, + addresses=$a($o(uri=http://sample/addressBook/ + address/1,personUri=http://sample/addressBook/ + person/1,id=$n(1),street=100+Main+Street,city= + Anywhereville,state=NY,zip=$n(12345),isCurrent=$b(true)))) + + A secondary "lax" syntax is available when the data type of the + values are already known on the receiving end of the transmission: + + x=(id=1,name=John+Smith,uri=http://sample/ + addressBook/person/1,addressBookUri=http://sample/ + addressBook,birthDate=1946-08-12T00:00:00Z,otherIds=%00, + addresses=((uri=http://sample/addressBook/ + address/1,personUri=http://sample/addressBook/ + person/1,id=1,street=100+Main+Street,city= + Anywhereville,state=NY,zip=12345,isCurrent=true))) + + Values represented in strict mode can be losslessly converted + back and forth into a JSON model without any additional + information. Values represented in lax mode cannot. + +1. Language constraints + + The grammar syntax is constrained to usage of characters allowed by + URI notation: + + uric = reserved | unreserved | escaped + reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | + "$" | "," + unreserved = alphanum | mark + mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")" + + In particular, the URI specification disallows the following + characters in unencoded form: + + unwise = "{" | "}" | "|" | "\" | "^" | "[" | "]" | "`" + delims = "<" | ">" | "#" | "%" | <"> + + The exclusion of {} and [] characters eliminates the possibility of + using JSON as parameter values. + + +2. Grammar constructs + + The grammar consists of the following language constructs: + + Objects - Values consisting of one or more child name/value pairs. + Arrays - Values consisting of zero or more child values. + Booleans - Values consisting of true/false values. + Numbers - Decimal and floating point values. + Strings - Everything else. + +2.1. Objects + + Objects are values consisting of one or more child name/value pairs. + The $o() construct is used to define an object. + + Example: A simple map with two key/value pairs: + + a1=$o(b1=x1,b2=x2) + + Example: A nested map: + + a1=$o(b1=$o(c1=x1,c2=x2)) + + When the data type is already known to be an object on the receiving + end, then the type flag can be removed from the construct to produce + a simplified value. + + Example: A nested map using "lax" syntax: + + a1=(b1=(c1=x1,c2=x2)) + +2.2. Arrays + + Arrays are values consisting of zero or more child values. + The $a() construct is used to define an array. + + Example: An array of two string values: + + a1=$a(x1,x2) + + Example: A 2-dimensional array: + + a1=$a($a(x1,x2),$a(x3,x4)) + + Example: An array of objects: + + a1=$a($o(b1=x1,b2=x2),$o(c1=x1,c2=x2)) + + When the data type is already known to be an array on the receiving + end, then the type flag can be removed from the construct to produce + a simplified value. + + Example: An array of objects using "lax" syntax: + + a1=((b1=x1,b2=x2),(c1=x1,c2=x2)) + +2.3. Booleans + + Booleans are values that can only take on values "true" or "false". + The $b() construct is used to define a boolean. + + Example: Two boolean values: + + a1=$b(true)&a2=$b(false) + + When the data type is already known to be a boolean on the receiving + end, then the type flag and parentheses can be removed from the + construct to produce a simplified value. + + Example: Two boolean values using "lax" syntax: + + a1=true&a2=false + +2.4. Numbers + + The $n() construct is used to define a number. + Both decimal and float numbers are supported. + + Example: Two numerical values, one decimal and one float: + + a1=$n(123)&a2=$n(1.23e1) + + When the data type is already known to be a number on the receiving + end, then the type flag and parentheses can be removed from the + construct to produce a simplified value. + + Example: Two numerical values using "lax" syntax: + + a1=123&a2=1.23e1 + +2.5. Strings + + Anything not conforming to one of the constructs described above + are treated as simple strings. + + Example: A simple string value: + + a1=foobar + + The tilde character (~) is used for escaping characters to prevent + them from being confused with syntax characters. + + The following characters must be escaped in string literals: + + $ , ( ) ~ = + + For example, the string literal "$o(b1=x)" should be + represented as follows: + + a1=~$o~(b1~=x~) + + In addition, strings can optionally be enclosed in parentheses + when needed to handle ambiguous cases. + + The following two values are equivalent: + + a1=foobar + a1=(foobar) + + Using parentheses, the following construct can be used to represent + an empty string: + + a1=() + + The purpose for this is to handle a potential ambiguity in the + representation of an empty array ([]) vs. an array containing one + empty string ([""]). An array containing one empty string is + represented as follows: + + a1=$a(()) + + Without this construct, there would not be a way to tell the + difference between an empty array and an array containing an empty + string: + + a1=$a() + + Note that an array consisting of two empty strings does not suffer + from this ambiguity, and the use of parenthesis is optional in + this case: + + a1=$a(,) + +2.7. Null values + + Nulls are represented by ASCII '0' as an escaped hex sequence: + + a1=%00 + + Note that a string consisting of a single null character can be + represented with the following construct: + + a1=(%00) + +2.8. Top-level attribute names + + Top-level attribute names (e.g. "a1" in "&a1=foobar") are treated + as strings but for one exception. The '=' character must be + encoded so as not to be confused as a key/value separator. + Note that the '=' character must also be escaped per the UON + notation. + + For example, the UON equivalent of {"a=b":"a=b"} constructed as + a top-level query parameter string would be as follows: + + a~%3Db=a~=b + + Note that the '=' character is encoded in the attribute name, + but it is not necessary to have it encoded in the attribute value. + +2.9. URL-encoded characters + + UON notation allows for any character, even UON grammar + characters, to be URL-encoded. + + The following query strings are fully equivalent in structure: + + a1=$o(b1=x1,b2=x2) + %61%31=%24%6F%28%62%31%3D%78%31%2C%62%32%3D%78%32%29 + + +3. BNF + + The following BNF describes the syntax for top-level URI query + parameter values (e.g. ?<attrname>=<value>). + + attrname = (string | null) + value = (var | string | null) + + string = ("(" litchar* ")") | litchar* + null = "%00" + + var = ovar | avar | nvar | bvar + ovar = ovar_strict | ovar_lax + avar = avar_strict | avar_lax + nvar = nvar_strict | nvar_lax + bvar = bvar_strict | bvar_lax + ovar_strict = "$o(" [pairs] ")" + ovar_lax = "(" [pairs] ")" + avar_strict = "$a(" [values] ")" + avar_lax = "(" [values] ")" + nvar_strict = "$n(" number ")" + nvar_lax = number + bvar_strict = "$b(" boolean ")" + bvar_lax = boolean + + pairs = pair ["," pairs] + pair = key "=" value + values = value ["," values] + key = (string | null) + boolean = "true" | "false" + + escape_seq = "~" escaped + encode_seq = "%" digithex digithex + + number = [-] (decimal | float) [exp] + decimal = "0" | (digit19 digit*) + float = decimal "." digit+ + exp = "e" [("+" | "-")] digit+ + + litchar = unencoded | encode_seq | escape_seq + escaped = "$" | "," | "(" | ")" | "~" | "=" + unencoded = alpha | digit | + ";" | "/" | "?" | ":" | "@" | + "-" | "_" | "." | "!" | "*" | "'" + alpha = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | + "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | + "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" | + "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | + "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | + "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" + digit = "0" | digit19 + digit19 = "1" | "2" | "3" | "4" | "5" | "6" | "7" | + "8" | "9" + digithex = digit | + "A" | "B" | "C" | "D" | "E" | "F" | + "a" | "b" | "c" | "d" | "e" | "f" + + + +
