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