001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.urlencoding; 018 019import java.io.*; 020import java.lang.reflect.*; 021import java.nio.charset.*; 022import java.util.*; 023import java.util.function.*; 024 025import org.apache.juneau.*; 026import org.apache.juneau.collections.*; 027import org.apache.juneau.common.utils.*; 028import org.apache.juneau.httppart.*; 029import org.apache.juneau.internal.*; 030import org.apache.juneau.parser.*; 031import org.apache.juneau.swap.*; 032import org.apache.juneau.uon.*; 033 034/** 035 * Session object that lives for the duration of a single use of {@link UrlEncodingParser}. 036 * 037 * <h5 class='section'>Notes:</h5><ul> 038 * <li class='warn'>This class is not thread safe and is typically discarded after one use. 039 * </ul> 040 * 041 * <h5 class='section'>See Also:</h5><ul> 042 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/UrlEncodingBasics">URL-Encoding Basics</a> 043 * </ul> 044 */ 045@SuppressWarnings({ "unchecked", "rawtypes" }) 046public class UrlEncodingParserSession extends UonParserSession { 047 048 //------------------------------------------------------------------------------------------------------------------- 049 // Static 050 //------------------------------------------------------------------------------------------------------------------- 051 052 /** 053 * Creates a new builder for this object. 054 * 055 * @param ctx The context creating this session. 056 * @return A new builder. 057 */ 058 public static Builder create(UrlEncodingParser ctx) { 059 return new Builder(ctx); 060 } 061 062 //------------------------------------------------------------------------------------------------------------------- 063 // Builder 064 //------------------------------------------------------------------------------------------------------------------- 065 066 /** 067 * Builder class. 068 */ 069 public static class Builder extends UonParserSession.Builder { 070 071 UrlEncodingParser ctx; 072 073 /** 074 * Constructor 075 * 076 * @param ctx The context creating this session. 077 */ 078 protected Builder(UrlEncodingParser ctx) { 079 super(ctx); 080 this.ctx = ctx; 081 } 082 083 @Override 084 public UrlEncodingParserSession build() { 085 return new UrlEncodingParserSession(this); 086 } 087 @Override /* Overridden from Builder */ 088 public <T> Builder apply(Class<T> type, Consumer<T> apply) { 089 super.apply(type, apply); 090 return this; 091 } 092 093 @Override /* Overridden from Builder */ 094 public Builder debug(Boolean value) { 095 super.debug(value); 096 return this; 097 } 098 099 @Override /* Overridden from Builder */ 100 public Builder properties(Map<String,Object> value) { 101 super.properties(value); 102 return this; 103 } 104 105 @Override /* Overridden from Builder */ 106 public Builder property(String key, Object value) { 107 super.property(key, value); 108 return this; 109 } 110 111 @Override /* Overridden from Builder */ 112 public Builder unmodifiable() { 113 super.unmodifiable(); 114 return this; 115 } 116 117 @Override /* Overridden from Builder */ 118 public Builder locale(Locale value) { 119 super.locale(value); 120 return this; 121 } 122 123 @Override /* Overridden from Builder */ 124 public Builder localeDefault(Locale value) { 125 super.localeDefault(value); 126 return this; 127 } 128 129 @Override /* Overridden from Builder */ 130 public Builder mediaType(MediaType value) { 131 super.mediaType(value); 132 return this; 133 } 134 135 @Override /* Overridden from Builder */ 136 public Builder mediaTypeDefault(MediaType value) { 137 super.mediaTypeDefault(value); 138 return this; 139 } 140 141 @Override /* Overridden from Builder */ 142 public Builder timeZone(TimeZone value) { 143 super.timeZone(value); 144 return this; 145 } 146 147 @Override /* Overridden from Builder */ 148 public Builder timeZoneDefault(TimeZone value) { 149 super.timeZoneDefault(value); 150 return this; 151 } 152 153 @Override /* Overridden from Builder */ 154 public Builder javaMethod(Method value) { 155 super.javaMethod(value); 156 return this; 157 } 158 159 @Override /* Overridden from Builder */ 160 public Builder outer(Object value) { 161 super.outer(value); 162 return this; 163 } 164 165 @Override /* Overridden from Builder */ 166 public Builder schema(HttpPartSchema value) { 167 super.schema(value); 168 return this; 169 } 170 171 @Override /* Overridden from Builder */ 172 public Builder schemaDefault(HttpPartSchema value) { 173 super.schemaDefault(value); 174 return this; 175 } 176 177 @Override /* Overridden from Builder */ 178 public Builder fileCharset(Charset value) { 179 super.fileCharset(value); 180 return this; 181 } 182 183 @Override /* Overridden from Builder */ 184 public Builder streamCharset(Charset value) { 185 super.streamCharset(value); 186 return this; 187 } 188 189 @Override /* Overridden from Builder */ 190 public Builder decoding(boolean value) { 191 super.decoding(value); 192 return this; 193 } 194 } 195 196 //------------------------------------------------------------------------------------------------------------------- 197 // Instance 198 //------------------------------------------------------------------------------------------------------------------- 199 200 private final UrlEncodingParser ctx; 201 202 /** 203 * Constructor. 204 * 205 * @param builder The builder for this object. 206 */ 207 public UrlEncodingParserSession(Builder builder) { 208 super(builder); 209 ctx = builder.ctx; 210 } 211 212 /** 213 * Returns <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs. 214 * 215 * @param pMeta The metadata on the bean property. 216 * @return <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs. 217 */ 218 public final boolean shouldUseExpandedParams(BeanPropertyMeta pMeta) { 219 ClassMeta<?> cm = pMeta.getClassMeta().getSerializedClassMeta(this); 220 if (cm.isCollectionOrArray()) { 221 if (isExpandedParams() || getUrlEncodingClassMeta(pMeta.getBeanMeta().getClassMeta()).isExpandedParams()) 222 return true; 223 } 224 return false; 225 } 226 227 @Override /* ParserSession */ 228 protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException { 229 try (UonReader r = getUonReader(pipe, true)) { 230 return parseAnything(type, r, getOuter()); 231 } 232 } 233 234 @Override /* ReaderParserSession */ 235 protected <K,V> Map<K,V> doParseIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws Exception { 236 try (UonReader r = getUonReader(pipe, true)) { 237 if (r.peekSkipWs() == '?') 238 r.read(); // NOSONAR - skip leading '?'. 239 m = parseIntoMap2(r, m, getClassMeta(Map.class, keyType, valueType), null); 240 return m; 241 } 242 } 243 244 private <T> T parseAnything(ClassMeta<T> eType, UonReader r, Object outer) throws IOException, ParseException, ExecutableException { 245 246 if (eType == null) 247 eType = (ClassMeta<T>)object(); 248 ObjectSwap<T,Object> swap = (ObjectSwap<T,Object>)eType.getSwap(this); 249 BuilderSwap<T,Object> builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this); 250 ClassMeta<?> sType = null; 251 if (builder != null) 252 sType = builder.getBuilderClassMeta(this); 253 else if (swap != null) 254 sType = swap.getSwapClassMeta(this); 255 else 256 sType = eType; 257 258 if (sType.isOptional()) 259 return (T)Utils.opt(parseAnything(eType.getElementType(), r, outer)); 260 261 int c = r.peekSkipWs(); 262 if (c == '?') 263 r.read(); // NOSONAR - skip leading '?'. 264 265 Object o; 266 267 if (sType.isObject()) { 268 JsonMap m = new JsonMap(this); 269 parseIntoMap2(r, m, getClassMeta(Map.class, String.class, Object.class), outer); 270 if (m.containsKey("_value")) 271 o = m.get("_value"); 272 else 273 o = cast(m, null, eType); 274 } else if (sType.isMap()) { 275 Map m = (sType.canCreateNewInstance() ? (Map)sType.newInstance() : newGenericMap(sType)); 276 o = parseIntoMap2(r, m, sType, m); 277 } else if (builder != null) { 278 BeanMap m = toBeanMap(builder.create(this, eType)); 279 m = parseIntoBeanMap(r, m); 280 o = m == null ? null : builder.build(this, m.getBean(), eType); 281 } else if (sType.canCreateNewBean(outer)) { 282 BeanMap m = newBeanMap(outer, sType.getInnerClass()); 283 m = parseIntoBeanMap(r, m); 284 o = m == null ? null : m.getBean(); 285 } else if (sType.isCollection() || sType.isArray() || sType.isArgs()) { 286 // ?1=foo&2=bar... 287 Collection c2 = ((sType.isArray() || sType.isArgs()) || ! sType.canCreateNewInstance(outer)) ? new JsonList(this) : (Collection)sType.newInstance(); 288 Map<Integer,Object> m = new TreeMap<>(); 289 parseIntoMap2(r, m, sType, c2); 290 c2.addAll(m.values()); 291 if (sType.isArray()) 292 o = ArrayUtils.toArray(c2, sType.getElementType().getInnerClass()); 293 else if (sType.isArgs()) 294 o = c2.toArray(new Object[c2.size()]); 295 else 296 o = c2; 297 } else { 298 // It could be a non-bean with _type attribute. 299 JsonMap m = new JsonMap(this); 300 parseIntoMap2(r, m, getClassMeta(Map.class, String.class, Object.class), outer); 301 if (m.containsKey(getBeanTypePropertyName(eType))) 302 o = cast(m, null, eType); 303 else if (m.containsKey("_value")) 304 o = convertToType(m.get("_value"), sType); 305 else if (sType.getProxyInvocationHandler() != null) { 306 o = newBeanMap(outer, sType.getInnerClass()).load(m).getBean(); 307 } else { 308 if (sType.getNotABeanReason() != null) 309 throw new ParseException(this, "Class ''{0}'' could not be instantiated as application/x-www-form-urlencoded. Reason: ''{1}''", sType, sType.getNotABeanReason()); 310 throw new ParseException(this, "Malformed application/x-www-form-urlencoded input for class ''{0}''.", sType); 311 } 312 } 313 314 if (swap != null && o != null) 315 o = unswap(swap, o, eType); 316 317 if (outer != null) 318 setParent(eType, o, outer); 319 320 return (T)o; 321 } 322 323 private <K,V> Map<K,V> parseIntoMap2(UonReader r, Map<K,V> m, ClassMeta<?> type, Object outer) throws IOException, ParseException, ExecutableException { 324 325 ClassMeta<K> keyType = (ClassMeta<K>)(type.isArgs() || type.isCollectionOrArray() ? getClassMeta(Integer.class) : type.getKeyType()); 326 327 int c = r.peekSkipWs(); 328 if (c == -1) 329 return m; 330 331 final int S1=1; // Looking for attrName start. 332 final int S2=2; // Found attrName end, looking for =. 333 final int S3=3; // Found =, looking for valStart. 334 final int S4=4; // Looking for & or end. 335 boolean isInEscape = false; 336 337 int state = S1; 338 int argIndex = 0; 339 K currAttr = null; 340 while (c != -1) { 341 c = r.read(); 342 if (! isInEscape) { 343 if (state == S1) { 344 if (c == -1) 345 return m; 346 r.unread(); 347 Object attr = parseAttr(r, true); 348 currAttr = attr == null ? null : convertAttrToType(m, trim(attr.toString()), keyType); 349 state = S2; 350 c = 0; // Avoid isInEscape if c was '\' 351 } else if (state == S2) { 352 if (c == '\u0002') 353 state = S3; 354 else if (c == -1 || c == '\u0001') { 355 m.put(currAttr, null); 356 if (c == -1) 357 return m; 358 state = S1; 359 } 360 } else if (state == S3) { 361 if (c == -1 || c == '\u0001') { 362 ClassMeta<V> valueType = (ClassMeta<V>)(type.isArgs() ? type.getArg(argIndex++) : type.isCollectionOrArray() ? type.getElementType() : type.getValueType()); 363 V value = convertAttrToType(m, "", valueType); 364 m.put(currAttr, value); 365 if (c == -1) 366 return m; 367 state = S1; 368 } else { 369 // For performance, we bypass parseAnything for string values. 370 ClassMeta<V> valueType = (ClassMeta<V>)(type.isArgs() ? type.getArg(argIndex++) : type.isCollectionOrArray() ? type.getElementType() : type.getValueType()); 371 V value = (V)(valueType.isString() ? super.parseString(r.unread(), true) : super.parseAnything(valueType, r.unread(), outer, true, null)); 372 373 // If we already encountered this parameter, turn it into a list. 374 if (m.containsKey(currAttr) && valueType.isObject()) { 375 Object v2 = m.get(currAttr); 376 if (! (v2 instanceof JsonList)) { 377 v2 = new JsonList(v2).setBeanSession(this); 378 m.put(currAttr, (V)v2); 379 } 380 ((JsonList)v2).add(value); 381 } else { 382 m.put(currAttr, value); 383 } 384 state = S4; 385 c = 0; // Avoid isInEscape if c was '\' 386 } 387 } else if (state == S4) { 388 if (c == '\u0001') 389 state = S1; 390 else if (c == -1) { 391 return m; 392 } 393 } 394 } 395 isInEscape = (c == '\\' && ! isInEscape); 396 } 397 if (state == S1) 398 throw new ParseException(this, "Could not find attribute name on object."); 399 if (state == S2) 400 throw new ParseException(this, "Could not find '=' following attribute name on object."); 401 if (state == S3) 402 throw new ParseException(this, "Dangling '=' found in object entry"); 403 if (state == S4) 404 throw new ParseException(this, "Could not find end of object."); 405 406 return null; // Unreachable. 407 } 408 409 private <T> BeanMap<T> parseIntoBeanMap(UonReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException { 410 411 int c = r.peekSkipWs(); 412 if (c == -1) 413 return m; 414 415 final int S1=1; // Looking for attrName start. 416 final int S2=2; // Found attrName end, looking for =. 417 final int S3=3; // Found =, looking for valStart. 418 final int S4=4; // Looking for , or } 419 boolean isInEscape = false; 420 421 int state = S1; 422 String currAttr = ""; 423 mark(); 424 try { 425 while (c != -1) { 426 c = r.read(); 427 if (! isInEscape) { 428 if (state == S1) { 429 if (c == -1) { 430 return m; 431 } 432 r.unread(); 433 mark(); 434 currAttr = parseAttrName(r, true); 435 if (currAttr == null) // Value was '%00' 436 return null; 437 state = S2; 438 } else if (state == S2) { 439 if (c == '\u0002') 440 state = S3; 441 else if (c == -1 || c == '\u0001') { 442 m.put(currAttr, null); 443 if (c == -1) 444 return m; 445 state = S1; 446 } 447 } else if (state == S3) { 448 if (c == -1 || c == '\u0001') { 449 if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) { 450 BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr); 451 if (pMeta == null) { 452 onUnknownProperty(currAttr, m, null); 453 unmark(); 454 } else { 455 unmark(); 456 setCurrentProperty(pMeta); 457 // In cases of "&foo=", create an empty instance of the value if createable. 458 // Otherwise, leave it null. 459 ClassMeta<?> cm = pMeta.getClassMeta(); 460 if (cm.canCreateNewInstance()) { 461 try { 462 pMeta.set(m, currAttr, cm.newInstance()); 463 } catch (BeanRuntimeException e) { 464 onBeanSetterException(pMeta, e); 465 throw e; 466 } 467 } 468 setCurrentProperty(null); 469 } 470 } 471 if (c == -1) 472 return m; 473 state = S1; 474 } else { 475 if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) { 476 BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr); 477 if (pMeta == null) { 478 onUnknownProperty(currAttr, m, parseAnything(object(), r.unread(), m.getBean(false), true, null)); 479 unmark(); 480 } else { 481 unmark(); 482 setCurrentProperty(pMeta); 483 if (shouldUseExpandedParams(pMeta)) { 484 ClassMeta et = pMeta.getClassMeta().getElementType(); 485 Object value = parseAnything(et, r.unread(), m.getBean(false), true, pMeta); 486 setName(et, value, currAttr); 487 try { 488 pMeta.add(m, currAttr, value); 489 } catch (BeanRuntimeException e) { 490 onBeanSetterException(pMeta, e); 491 throw e; 492 } 493 } else { 494 ClassMeta<?> cm = pMeta.getClassMeta(); 495 Object value = parseAnything(cm, r.unread(), m.getBean(false), true, pMeta); 496 setName(cm, value, currAttr); 497 try { 498 pMeta.set(m, currAttr, value); 499 } catch (BeanRuntimeException e) { 500 onBeanSetterException(pMeta, e); 501 throw e; 502 } 503 } 504 setCurrentProperty(null); 505 } 506 } 507 state = S4; 508 } 509 } else if (state == S4) { 510 if (c == '\u0001') 511 state = S1; 512 else if (c == -1) { 513 return m; 514 } 515 } 516 } 517 isInEscape = (c == '\\' && ! isInEscape); 518 } 519 if (state == S1) 520 throw new ParseException(this, "Could not find attribute name on object."); 521 if (state == S2) 522 throw new ParseException(this, "Could not find '=' following attribute name on object."); 523 if (state == S3) 524 throw new ParseException(this, "Could not find value following '=' on object."); 525 if (state == S4) 526 throw new ParseException(this, "Could not find end of object."); 527 } finally { 528 unmark(); 529 } 530 531 return null; // Unreachable. 532 } 533 534 //----------------------------------------------------------------------------------------------------------------- 535 // Properties 536 //----------------------------------------------------------------------------------------------------------------- 537 538 /** 539 * Parser bean property collections/arrays as separate key/value pairs. 540 * 541 * @see UrlEncodingParser.Builder#expandedParams() 542 * @return 543 * <jk>false</jk> if serializing the array <c>[1,2,3]</c> results in <c>?key=$a(1,2,3)</c>. 544 * <br><jk>true</jk> if serializing the same array results in <c>?key=1&key=2&key=3</c>. 545 */ 546 protected final boolean isExpandedParams() { 547 return ctx.isExpandedParams(); 548 } 549 550 //----------------------------------------------------------------------------------------------------------------- 551 // Extended metadata 552 //----------------------------------------------------------------------------------------------------------------- 553 554 /** 555 * Returns the language-specific metadata on the specified class. 556 * 557 * @param cm The class to return the metadata on. 558 * @return The metadata. 559 */ 560 protected UrlEncodingClassMeta getUrlEncodingClassMeta(ClassMeta<?> cm) { 561 return ctx.getUrlEncodingClassMeta(cm); 562 } 563}