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