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