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 (pMeta.getBeanMeta().getClassMeta().getExtendedMeta(UrlEncodingClassMeta.class).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 int c = r.peekSkipWs(); 100 if (c == '?') 101 r.read(); 102 103 Object o; 104 105 if (sType.isObject()) { 106 ObjectMap m = new ObjectMap(this); 107 parseIntoMap2(r, m, getClassMeta(Map.class, String.class, Object.class), outer); 108 if (m.containsKey("_value")) 109 o = m.get("_value"); 110 else 111 o = cast(m, null, eType); 112 } else if (sType.isMap()) { 113 Map m = (sType.canCreateNewInstance() ? (Map)sType.newInstance() : new ObjectMap(this)); 114 o = parseIntoMap2(r, m, sType, m); 115 } else if (builder != null) { 116 BeanMap m = toBeanMap(builder.create(this, eType)); 117 m = parseIntoBeanMap(r, m); 118 o = m == null ? null : builder.build(this, m.getBean(), eType); 119 } else if (sType.canCreateNewBean(outer)) { 120 BeanMap m = newBeanMap(outer, sType.getInnerClass()); 121 m = parseIntoBeanMap(r, m); 122 o = m == null ? null : m.getBean(); 123 } else if (sType.isCollection() || sType.isArray() || sType.isArgs()) { 124 // ?1=foo&2=bar... 125 Collection c2 = ((sType.isArray() || sType.isArgs()) || ! sType.canCreateNewInstance(outer)) ? new ObjectList(this) : (Collection)sType.newInstance(); 126 Map<Integer,Object> m = new TreeMap<>(); 127 parseIntoMap2(r, m, sType, c2); 128 c2.addAll(m.values()); 129 if (sType.isArray()) 130 o = ArrayUtils.toArray(c2, sType.getElementType().getInnerClass()); 131 else if (sType.isArgs()) 132 o = c2.toArray(new Object[c2.size()]); 133 else 134 o = c2; 135 } else { 136 // It could be a non-bean with _type attribute. 137 ObjectMap m = new ObjectMap(this); 138 parseIntoMap2(r, m, getClassMeta(Map.class, String.class, Object.class), outer); 139 if (m.containsKey(getBeanTypePropertyName(eType))) 140 o = cast(m, null, eType); 141 else if (m.containsKey("_value")) { 142 o = convertToType(m.get("_value"), sType); 143 } else { 144 if (sType.getNotABeanReason() != null) 145 throw new ParseException(this, "Class ''{0}'' could not be instantiated as application/x-www-form-urlencoded. Reason: ''{1}''", sType, sType.getNotABeanReason()); 146 throw new ParseException(this, "Malformed application/x-www-form-urlencoded input for class ''{0}''.", sType); 147 } 148 } 149 150 if (swap != null && o != null) 151 o = unswap(swap, o, eType); 152 153 if (outer != null) 154 setParent(eType, o, outer); 155 156 return (T)o; 157 } 158 159 private <K,V> Map<K,V> parseIntoMap2(UonReader r, Map<K,V> m, ClassMeta<?> type, Object outer) throws IOException, ParseException, ExecutableException { 160 161 ClassMeta<K> keyType = (ClassMeta<K>)(type.isArgs() || type.isCollectionOrArray() ? getClassMeta(Integer.class) : type.getKeyType()); 162 163 int c = r.peekSkipWs(); 164 if (c == -1) 165 return m; 166 167 final int S1=1; // Looking for attrName start. 168 final int S2=2; // Found attrName end, looking for =. 169 final int S3=3; // Found =, looking for valStart. 170 final int S4=4; // Looking for & or end. 171 boolean isInEscape = false; 172 173 int state = S1; 174 int argIndex = 0; 175 K currAttr = null; 176 while (c != -1) { 177 c = r.read(); 178 if (! isInEscape) { 179 if (state == S1) { 180 if (c == -1) 181 return m; 182 r.unread(); 183 Object attr = parseAttr(r, true); 184 currAttr = attr == null ? null : convertAttrToType(m, trim(attr.toString()), keyType); 185 state = S2; 186 c = 0; // Avoid isInEscape if c was '\' 187 } else if (state == S2) { 188 if (c == '\u0002') 189 state = S3; 190 else if (c == -1 || c == '\u0001') { 191 m.put(currAttr, null); 192 if (c == -1) 193 return m; 194 state = S1; 195 } 196 } else if (state == S3) { 197 if (c == -1 || c == '\u0001') { 198 ClassMeta<V> valueType = (ClassMeta<V>)(type.isArgs() ? type.getArg(argIndex++) : type.isCollectionOrArray() ? type.getElementType() : type.getValueType()); 199 V value = convertAttrToType(m, "", valueType); 200 m.put(currAttr, value); 201 if (c == -1) 202 return m; 203 state = S1; 204 } else { 205 // For performance, we bypass parseAnything for string values. 206 ClassMeta<V> valueType = (ClassMeta<V>)(type.isArgs() ? type.getArg(argIndex++) : type.isCollectionOrArray() ? type.getElementType() : type.getValueType()); 207 V value = (V)(valueType.isString() ? super.parseString(r.unread(), true) : super.parseAnything(valueType, r.unread(), outer, true, null)); 208 209 // If we already encountered this parameter, turn it into a list. 210 if (m.containsKey(currAttr) && valueType.isObject()) { 211 Object v2 = m.get(currAttr); 212 if (! (v2 instanceof ObjectList)) { 213 v2 = new ObjectList(v2).setBeanSession(this); 214 m.put(currAttr, (V)v2); 215 } 216 ((ObjectList)v2).add(value); 217 } else { 218 m.put(currAttr, value); 219 } 220 state = S4; 221 c = 0; // Avoid isInEscape if c was '\' 222 } 223 } else if (state == S4) { 224 if (c == '\u0001') 225 state = S1; 226 else if (c == -1) { 227 return m; 228 } 229 } 230 } 231 isInEscape = (c == '\\' && ! isInEscape); 232 } 233 if (state == S1) 234 throw new ParseException(this, "Could not find attribute name on object."); 235 if (state == S2) 236 throw new ParseException(this, "Could not find '=' following attribute name on object."); 237 if (state == S3) 238 throw new ParseException(this, "Dangling '=' found in object entry"); 239 if (state == S4) 240 throw new ParseException(this, "Could not find end of object."); 241 242 return null; // Unreachable. 243 } 244 245 private <T> BeanMap<T> parseIntoBeanMap(UonReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException { 246 247 int c = r.peekSkipWs(); 248 if (c == -1) 249 return m; 250 251 final int S1=1; // Looking for attrName start. 252 final int S2=2; // Found attrName end, looking for =. 253 final int S3=3; // Found =, looking for valStart. 254 final int S4=4; // Looking for , or } 255 boolean isInEscape = false; 256 257 int state = S1; 258 String currAttr = ""; 259 mark(); 260 try { 261 while (c != -1) { 262 c = r.read(); 263 if (! isInEscape) { 264 if (state == S1) { 265 if (c == -1) { 266 return m; 267 } 268 r.unread(); 269 mark(); 270 currAttr = parseAttrName(r, true); 271 if (currAttr == null) // Value was '%00' 272 return null; 273 state = S2; 274 } else if (state == S2) { 275 if (c == '\u0002') 276 state = S3; 277 else if (c == -1 || c == '\u0001') { 278 m.put(currAttr, null); 279 if (c == -1) 280 return m; 281 state = S1; 282 } 283 } else if (state == S3) { 284 if (c == -1 || c == '\u0001') { 285 if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) { 286 BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr); 287 if (pMeta == null) { 288 onUnknownProperty(currAttr, m); 289 unmark(); 290 } else { 291 unmark(); 292 setCurrentProperty(pMeta); 293 // In cases of "&foo=", create an empty instance of the value if createable. 294 // Otherwise, leave it null. 295 ClassMeta<?> cm = pMeta.getClassMeta(); 296 if (cm.canCreateNewInstance()) 297 pMeta.set(m, currAttr, cm.newInstance()); 298 setCurrentProperty(null); 299 } 300 } 301 if (c == -1) 302 return m; 303 state = S1; 304 } else { 305 if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) { 306 BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr); 307 if (pMeta == null) { 308 onUnknownProperty(currAttr, m); 309 unmark(); 310 parseAnything(object(), r.unread(), m.getBean(false), true, null); // Read content anyway to ignore it 311 } else { 312 unmark(); 313 setCurrentProperty(pMeta); 314 if (shouldUseExpandedParams(pMeta)) { 315 ClassMeta et = pMeta.getClassMeta().getElementType(); 316 Object value = parseAnything(et, r.unread(), m.getBean(false), true, pMeta); 317 setName(et, value, currAttr); 318 pMeta.add(m, currAttr, value); 319 } else { 320 ClassMeta<?> cm = pMeta.getClassMeta(); 321 Object value = parseAnything(cm, r.unread(), m.getBean(false), true, pMeta); 322 setName(cm, value, currAttr); 323 pMeta.set(m, currAttr, value); 324 } 325 setCurrentProperty(null); 326 } 327 } 328 state = S4; 329 } 330 } else if (state == S4) { 331 if (c == '\u0001') 332 state = S1; 333 else if (c == -1) { 334 return m; 335 } 336 } 337 } 338 isInEscape = (c == '\\' && ! isInEscape); 339 } 340 if (state == S1) 341 throw new ParseException(this, "Could not find attribute name on object."); 342 if (state == S2) 343 throw new ParseException(this, "Could not find '=' following attribute name on object."); 344 if (state == S3) 345 throw new ParseException(this, "Could not find value following '=' on object."); 346 if (state == S4) 347 throw new ParseException(this, "Could not find end of object."); 348 } finally { 349 unmark(); 350 } 351 352 return null; // Unreachable. 353 } 354 355 //----------------------------------------------------------------------------------------------------------------- 356 // Properties 357 //----------------------------------------------------------------------------------------------------------------- 358 359 /** 360 * Configuration property: Parser bean property collections/arrays as separate key/value pairs. 361 * 362 * @see UrlEncodingParser#URLENC_expandedParams 363 * @return 364 * <jk>false</jk> if serializing the array <c>[1,2,3]</c> results in <c>?key=$a(1,2,3)</c>. 365 * <br><jk>true</jk> if serializing the same array results in <c>?key=1&key=2&key=3</c>. 366 */ 367 protected final boolean isExpandedParams() { 368 return ctx.isExpandedParams(); 369 } 370 371 //----------------------------------------------------------------------------------------------------------------- 372 // Other methods 373 //----------------------------------------------------------------------------------------------------------------- 374 375 @Override /* Session */ 376 public ObjectMap toMap() { 377 return super.toMap() 378 .append("UrlEncodingParserSession", new DefaultFilteringObjectMap() 379 ); 380 } 381}