001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.urlencoding; 014 015import static org.apache.juneau.urlencoding.UrlEncodingParser.*; 016 017import java.lang.reflect.*; 018import java.util.*; 019 020import org.apache.juneau.*; 021import org.apache.juneau.internal.*; 022import org.apache.juneau.parser.*; 023import org.apache.juneau.transform.*; 024import org.apache.juneau.uon.*; 025 026/** 027 * Session object that lives for the duration of a single use of {@link UrlEncodingParser}. 028 * 029 * <p> 030 * This class is NOT thread safe. 031 * It is typically discarded after one-time use although it can be reused against multiple inputs. 032 */ 033@SuppressWarnings({ "unchecked", "rawtypes" }) 034public class UrlEncodingParserSession extends UonParserSession { 035 036 private final boolean expandedParams; 037 038 /** 039 * Create a new session using properties specified in the context. 040 * 041 * @param ctx 042 * The context creating this session object. 043 * The context contains all the configuration settings for this object. 044 * @param args 045 * Runtime session arguments. 046 */ 047 protected UrlEncodingParserSession(UrlEncodingParser ctx, ParserSessionArgs args) { 048 super(ctx, args); 049 expandedParams = getProperty(URLENC_expandedParams, boolean.class, ctx.expandedParams); 050 } 051 052 @Override /* Session */ 053 public ObjectMap asMap() { 054 return super.asMap() 055 .append("UrlEncodingParser", new ObjectMap() 056 .append("expandedParams", expandedParams) 057 ); 058 } 059 060 /** 061 * Returns <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs. 062 * 063 * @param pMeta The metadata on the bean property. 064 * @return <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs. 065 */ 066 public final boolean shouldUseExpandedParams(BeanPropertyMeta pMeta) { 067 ClassMeta<?> cm = pMeta.getClassMeta().getSerializedClassMeta(this); 068 if (cm.isCollectionOrArray()) { 069 if (expandedParams) 070 return true; 071 if (pMeta.getBeanMeta().getClassMeta().getExtendedMeta(UrlEncodingClassMeta.class).isExpandedParams()) 072 return true; 073 } 074 return false; 075 } 076 077 @Override /* ParserSession */ 078 protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws Exception { 079 try (UonReader r = getUonReader(pipe, true)) { 080 return parseAnything(type, r, getOuter()); 081 } 082 } 083 084 @Override /* ReaderParserSession */ 085 protected <K,V> Map<K,V> doParseIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws Exception { 086 try (UonReader r = getUonReader(pipe, true)) { 087 if (r.peekSkipWs() == '?') 088 r.read(); 089 m = parseIntoMap2(r, m, getClassMeta(Map.class, keyType, valueType), null); 090 return m; 091 } 092 } 093 094 private <T> T parseAnything(ClassMeta<T> eType, UonReader r, Object outer) throws Exception { 095 096 if (eType == null) 097 eType = (ClassMeta<T>)object(); 098 PojoSwap<T,Object> swap = (PojoSwap<T,Object>)eType.getPojoSwap(this); 099 BuilderSwap<T,Object> builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this); 100 ClassMeta<?> sType = null; 101 if (builder != null) 102 sType = builder.getBuilderClassMeta(this); 103 else if (swap != null) 104 sType = swap.getSwapClassMeta(this); 105 else 106 sType = eType; 107 108 int c = r.peekSkipWs(); 109 if (c == '?') 110 r.read(); 111 112 Object o; 113 114 if (sType.isObject()) { 115 ObjectMap m = new ObjectMap(this); 116 parseIntoMap2(r, m, getClassMeta(Map.class, String.class, Object.class), outer); 117 if (m.containsKey("_value")) 118 o = m.get("_value"); 119 else 120 o = cast(m, null, eType); 121 } else if (sType.isMap()) { 122 Map m = (sType.canCreateNewInstance() ? (Map)sType.newInstance() : new ObjectMap(this)); 123 o = parseIntoMap2(r, m, sType, m); 124 } else if (builder != null) { 125 BeanMap m = toBeanMap(builder.create(this, eType)); 126 m = parseIntoBeanMap(r, m); 127 o = m == null ? null : builder.build(this, m.getBean(), eType); 128 } else if (sType.canCreateNewBean(outer)) { 129 BeanMap m = newBeanMap(outer, sType.getInnerClass()); 130 m = parseIntoBeanMap(r, m); 131 o = m == null ? null : m.getBean(); 132 } else if (sType.isCollection() || sType.isArray() || sType.isArgs()) { 133 // ?1=foo&2=bar... 134 Collection c2 = ((sType.isArray() || sType.isArgs()) || ! sType.canCreateNewInstance(outer)) ? new ObjectList(this) : (Collection)sType.newInstance(); 135 Map<Integer,Object> m = new TreeMap<>(); 136 parseIntoMap2(r, m, sType, c2); 137 c2.addAll(m.values()); 138 if (sType.isArray()) 139 o = ArrayUtils.toArray(c2, sType.getElementType().getInnerClass()); 140 else if (sType.isArgs()) 141 o = c2.toArray(new Object[c2.size()]); 142 else 143 o = c2; 144 } else { 145 // It could be a non-bean with _type attribute. 146 ObjectMap m = new ObjectMap(this); 147 parseIntoMap2(r, m, getClassMeta(Map.class, String.class, Object.class), outer); 148 if (m.containsKey(getBeanTypePropertyName(eType))) 149 o = cast(m, null, eType); 150 else if (m.containsKey("_value")) { 151 o = convertToType(m.get("_value"), sType); 152 } else { 153 if (sType.getNotABeanReason() != null) 154 throw new ParseException(loc(r), "Class ''{0}'' could not be instantiated as application/x-www-form-urlencoded. Reason: ''{1}''", sType, sType.getNotABeanReason()); 155 throw new ParseException(loc(r), "Malformed application/x-www-form-urlencoded input for class ''{0}''.", sType); 156 } 157 } 158 159 if (swap != null && o != null) 160 o = swap.unswap(this, o, eType); 161 162 if (outer != null) 163 setParent(eType, o, outer); 164 165 return (T)o; 166 } 167 168 private <K,V> Map<K,V> parseIntoMap2(UonReader r, Map<K,V> m, ClassMeta<?> type, Object outer) throws Exception { 169 170 ClassMeta<K> keyType = (ClassMeta<K>)(type.isArgs() || type.isCollectionOrArray() ? getClassMeta(Integer.class) : type.getKeyType()); 171 172 int c = r.peekSkipWs(); 173 if (c == -1) 174 return m; 175 176 final int S1=1; // Looking for attrName start. 177 final int S2=2; // Found attrName end, looking for =. 178 final int S3=3; // Found =, looking for valStart. 179 final int S4=4; // Looking for & or end. 180 boolean isInEscape = false; 181 182 int state = S1; 183 int argIndex = 0; 184 K currAttr = null; 185 while (c != -1) { 186 c = r.read(); 187 if (! isInEscape) { 188 if (state == S1) { 189 if (c == -1) 190 return m; 191 r.unread(); 192 Object attr = parseAttr(r, true); 193 currAttr = attr == null ? null : convertAttrToType(m, trim(attr.toString()), keyType); 194 state = S2; 195 c = 0; // Avoid isInEscape if c was '\' 196 } else if (state == S2) { 197 if (c == '\u0002') 198 state = S3; 199 else if (c == -1 || c == '\u0001') { 200 m.put(currAttr, null); 201 if (c == -1) 202 return m; 203 state = S1; 204 } 205 } else if (state == S3) { 206 if (c == -1 || c == '\u0001') { 207 ClassMeta<V> valueType = (ClassMeta<V>)(type.isArgs() ? type.getArg(argIndex++) : type.isCollectionOrArray() ? type.getElementType() : type.getValueType()); 208 V value = convertAttrToType(m, "", valueType); 209 m.put(currAttr, value); 210 if (c == -1) 211 return m; 212 state = S1; 213 } else { 214 // For performance, we bypass parseAnything for string values. 215 ClassMeta<V> valueType = (ClassMeta<V>)(type.isArgs() ? type.getArg(argIndex++) : type.isCollectionOrArray() ? type.getElementType() : type.getValueType()); 216 V value = (V)(valueType.isString() ? super.parseString(r.unread(), true) : super.parseAnything(valueType, r.unread(), outer, true, null)); 217 218 // If we already encountered this parameter, turn it into a list. 219 if (m.containsKey(currAttr) && valueType.isObject()) { 220 Object v2 = m.get(currAttr); 221 if (! (v2 instanceof ObjectList)) { 222 v2 = new ObjectList(v2).setBeanSession(this); 223 m.put(currAttr, (V)v2); 224 } 225 ((ObjectList)v2).add(value); 226 } else { 227 m.put(currAttr, value); 228 } 229 state = S4; 230 c = 0; // Avoid isInEscape if c was '\' 231 } 232 } else if (state == S4) { 233 if (c == '\u0001') 234 state = S1; 235 else if (c == -1) { 236 return m; 237 } 238 } 239 } 240 isInEscape = (c == '\\' && ! isInEscape); 241 } 242 if (state == S1) 243 throw new ParseException(loc(r), "Could not find attribute name on object."); 244 if (state == S2) 245 throw new ParseException(loc(r), "Could not find '=' following attribute name on object."); 246 if (state == S3) 247 throw new ParseException(loc(r), "Dangling '=' found in object entry"); 248 if (state == S4) 249 throw new ParseException(loc(r), "Could not find end of object."); 250 251 return null; // Unreachable. 252 } 253 254 private <T> BeanMap<T> parseIntoBeanMap(UonReader r, BeanMap<T> m) throws Exception { 255 256 int c = r.peekSkipWs(); 257 if (c == -1) 258 return m; 259 260 final int S1=1; // Looking for attrName start. 261 final int S2=2; // Found attrName end, looking for =. 262 final int S3=3; // Found =, looking for valStart. 263 final int S4=4; // Looking for , or } 264 boolean isInEscape = false; 265 266 int state = S1; 267 String currAttr = ""; 268 int currAttrLine = -1, currAttrCol = -1; 269 while (c != -1) { 270 c = r.read(); 271 if (! isInEscape) { 272 if (state == S1) { 273 if (c == -1) { 274 return m; 275 } 276 r.unread(); 277 currAttrLine= r.getLine(); 278 currAttrCol = r.getColumn(); 279 currAttr = parseAttrName(r, true); 280 if (currAttr == null) // Value was '%00' 281 return null; 282 state = S2; 283 } else if (state == S2) { 284 if (c == '\u0002') 285 state = S3; 286 else if (c == -1 || c == '\u0001') { 287 m.put(currAttr, null); 288 if (c == -1) 289 return m; 290 state = S1; 291 } 292 } else if (state == S3) { 293 if (c == -1 || c == '\u0001') { 294 if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) { 295 BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr); 296 if (pMeta == null) { 297 onUnknownProperty(r.getPipe(), currAttr, m, currAttrLine, currAttrCol); 298 } else { 299 setCurrentProperty(pMeta); 300 // In cases of "&foo=", create an empty instance of the value if createable. 301 // Otherwise, leave it null. 302 ClassMeta<?> cm = pMeta.getClassMeta(); 303 if (cm.canCreateNewInstance()) 304 pMeta.set(m, currAttr, cm.newInstance()); 305 setCurrentProperty(null); 306 } 307 } 308 if (c == -1) 309 return m; 310 state = S1; 311 } else { 312 if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) { 313 BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr); 314 if (pMeta == null) { 315 onUnknownProperty(r.getPipe(), currAttr, m, currAttrLine, currAttrCol); 316 parseAnything(object(), r.unread(), m.getBean(false), true, null); // Read content anyway to ignore it 317 } else { 318 setCurrentProperty(pMeta); 319 if (shouldUseExpandedParams(pMeta)) { 320 ClassMeta et = pMeta.getClassMeta().getElementType(); 321 Object value = parseAnything(et, r.unread(), m.getBean(false), true, pMeta); 322 setName(et, value, currAttr); 323 pMeta.add(m, currAttr, value); 324 } else { 325 ClassMeta<?> cm = pMeta.getClassMeta(); 326 Object value = parseAnything(cm, r.unread(), m.getBean(false), true, pMeta); 327 setName(cm, value, currAttr); 328 pMeta.set(m, currAttr, value); 329 } 330 setCurrentProperty(null); 331 } 332 } 333 state = S4; 334 } 335 } else if (state == S4) { 336 if (c == '\u0001') 337 state = S1; 338 else if (c == -1) { 339 return m; 340 } 341 } 342 } 343 isInEscape = (c == '\\' && ! isInEscape); 344 } 345 if (state == S1) 346 throw new ParseException(loc(r), "Could not find attribute name on object."); 347 if (state == S2) 348 throw new ParseException(loc(r), "Could not find '=' following attribute name on object."); 349 if (state == S3) 350 throw new ParseException(loc(r), "Could not find value following '=' on object."); 351 if (state == S4) 352 throw new ParseException(loc(r), "Could not find end of object."); 353 354 return null; // Unreachable. 355 } 356}