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.marshall.urlencoding; 018 019import static org.apache.juneau.commons.lang.StateEnum.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021 022import java.io.*; 023import java.lang.reflect.*; 024import java.util.*; 025 026import org.apache.juneau.commons.bean.*; 027import org.apache.juneau.commons.reflect.*; 028import org.apache.juneau.commons.utils.*; 029import org.apache.juneau.marshall.*; 030import org.apache.juneau.marshall.collections.*; 031import org.apache.juneau.marshall.parser.*; 032import org.apache.juneau.marshall.swap.spi.*; 033import org.apache.juneau.marshall.uon.*; 034 035/** 036 * Session object that lives for the duration of a single use of {@link UrlEncodingParser}. 037 * 038 * <h5 class='section'>Notes:</h5><ul> 039 * <li class='warn'>This class is not thread safe and is typically discarded after one use. 040 * </ul> 041 * 042 * <h5 class='section'>See Also:</h5><ul> 043 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/UrlEncodingSupport">URL-Encoding</a> 044 * </ul> 045 */ 046@SuppressWarnings({ 047 "java:S110", // Inheritance depth acceptable for session hierarchy 048 "java:S115", // Constants use UPPER_snakeCase convention (e.g., CONST_value) 049 "java:S1168", // readIntoBeanMap()/readIntoMap2() return null for blank or literal null input, which callers treat as 'no value'. 050 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 051 "java:S3776", // readAnything(), readIntoBeanMap() and readIntoMap2() are state machines over the URL-encoded grammar; splitting them would obscure the parse flow. 052 "rawtypes", // Raw types necessary for generic type handling 053 "resource", // UonReader is managed by caller 054 "unchecked", // Type erasure requires unchecked casts 055 "unused" // Intentionally unused; variable/parameter is required by the interface contract 056}) 057public class UrlEncodingParserSession extends UonParserSession { 058 059 /** 060 * Builder class. 061 */ 062 public static class Builder extends UonParserSession.Builder<Builder> { 063 064 private boolean expandedParams; 065 private final UrlEncodingParser ctx; 066 067 /** 068 * Constructor 069 * 070 * @param ctx The context creating this session. 071 * <br>Cannot be <jk>null</jk>. 072 */ 073 protected Builder(UrlEncodingParser ctx) { 074 super(ctx); 075 this.ctx = ctx; 076 expandedParams = ctx.isExpandedParams(); 077 } 078 079 @Override 080 public UrlEncodingParserSession build() { 081 return new UrlEncodingParserSession(this); 082 } 083 084 /** 085 * Serialize bean property collections/arrays as separate key/value pairs. 086 * 087 * @param value The new value for this setting. 088 * @return This object. 089 */ 090 public Builder expandedParams(boolean value) { 091 expandedParams = value; 092 return this; 093 } 094 095 @Override /* Overridden from Builder */ 096 public Builder property(String key, Object value) { 097 if (key == null) { super.property(key, value); return this; } 098 switch (key) { 099 case "expandedParams", "UrlEncodingParserSession.expandedParams": 100 return expandedParams(cvt(value, Boolean.class)); 101 default: 102 super.property(key, value); 103 return this; 104 } 105 } 106 107 } 108 109 /** 110 * Creates a new builder for this object. 111 * 112 * @param ctx The context creating this session. 113 * <br>Cannot be <jk>null</jk>. 114 * @return A new builder. 115 */ 116 public static Builder create(UrlEncodingParser ctx) { 117 return new Builder(ctx); 118 } 119 120 private final UrlEncodingParser ctx; 121 private final boolean expandedParams; 122 123 /** 124 * Constructor. 125 * 126 * @param builder The builder for this object. 127 */ 128 public UrlEncodingParserSession(Builder builder) { 129 super(builder); 130 ctx = builder.ctx; 131 expandedParams = builder.expandedParams; 132 } 133 134 /** 135 * Returns <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs. 136 * 137 * @param pMeta The metadata on the bean property. 138 * @return <jk>true</jk> if the specified bean property should be expanded as multiple key-value pairs. 139 */ 140 public final boolean shouldUseExpandedParams(BeanPropertyMeta pMeta) { 141 var cm = ((ClassMeta<?>) pMeta.getBeanInfo()).getSerializedClassMeta(this); 142 return cm.isCollectionOrArray() && (isExpandedParams() || getUrlEncodingClassMeta((ClassMeta<?>) pMeta.getBeanMeta().getBeanInfo()).isExpandedParams()); 143 } 144 145 private <T> T readAnything(ClassMeta<T> eType, UonReader r, Object outer) throws IOException, ParseException, ExecutableException { 146 147 if (eType == null) 148 eType = (ClassMeta<T>)object(); 149 var swap = (ObjectSwap<T,Object>)eType.getSwap(this); 150 var builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this); 151 ClassMeta<?> sType = null; 152 if (nn(builder)) 153 sType = builder.getBuilderClassMeta(this); 154 else if (nn(swap)) 155 sType = swap.getSwapClassMeta(this); 156 else 157 sType = eType; 158 159 if (sType.isOptional()) 160 return (T)o(readAnything(eType.getElementType(), r, outer)); 161 162 int c = r.peekSkipWs(); 163 if (c == '?') { 164 int ignored = r.read(); 165 } 166 167 Object o; 168 169 if (sType.isObject()) { 170 var m = newGenericMap(); 171 readIntoMap2(r, m, getClassMeta(Map.class, String.class, Object.class), outer); 172 if (m.containsKey("_value")) 173 o = m.get("_value"); 174 else 175 o = cast(m, null, eType); 176 } else if (nn(builder)) { 177 var m = toBeanMap(builder.create(this, eType)); 178 m = readIntoBeanMap(r, m); 179 o = m == null ? null : builder.build(this, checkRequired(m).getBean(), eType); 180 } else if (sType.canCreateNewBean(outer)) { 181 var m = newBeanMap(outer, sType.inner()); 182 m = readIntoBeanMap(r, m); 183 o = m == null ? null : checkRequired(m).getBean(); 184 } else if (sType.isMap()) { 185 var m = (sType.canCreateNewInstance() ? (Map)sType.newInstance() : newGenericMap(sType)); 186 o = readIntoMap2(r, m, sType, m); 187 } else if (sType.isCollection() || sType.isArray() || sType.isArgs()) { 188 // ?1=foo&2=bar... 189 var c2 = ((sType.isArray() || sType.isArgs()) || ! sType.canCreateNewInstance(outer)) ? newGenericList() : (Collection)sType.newInstance(); 190 var m = new TreeMap<Integer,Object>(); 191 readIntoMap2(r, m, sType, c2); 192 c2.addAll(m.values()); 193 if (sType.isArgs()) 194 o = c2.toArray(new Object[c2.size()]); 195 else if (sType.isArray()) 196 o = CollectionUtils.toArray(c2, sType.getElementType().inner()); 197 else 198 o = c2; 199 } else { 200 // It could be a non-bean with _type attribute. 201 var m = newGenericMap(); 202 readIntoMap2(r, m, getClassMeta(Map.class, String.class, Object.class), outer); 203 if (m.containsKey(getBeanTypePropertyName(eType))) 204 o = cast(m, null, eType); 205 else if (m.containsKey("_value")) 206 o = unwrapValueAs(m.get("_value"), sType); 207 else if (nn(sType.getProxyInvocationHandler())) { 208 o = checkRequired(newBeanMap(outer, sType.inner()).load(m)).getBean(); 209 } else { 210 if (nn(sType.getNotABeanReason())) 211 throw new ParseException(this, "Class '%s' could not be instantiated as application/x-www-form-urlencoded. Reason: '%s'", sType, sType.getNotABeanReason()); 212 throw new ParseException(this, "Malformed application/x-www-form-urlencoded input for class '%s'.", sType); 213 } 214 } 215 216 if (nn(swap) && nn(o)) 217 o = unswap(swap, o, eType); 218 219 if (nn(parentBean())) 220 setParent(eType, o, parentBean()); 221 222 return (T)o; 223 } 224 225 @SuppressWarnings({ 226 "java:S125", // State-machine comments (S1: ..., S2: ...) 227 "java:S2177", // Intentional: UrlEncodingParserSession provides its own private readIntoBeanMap() with expanded-params logic 228 "java:S2583", // State variables persist across loop iterations 229 "java:S2589", // Final if (state==S4) is always true given prior checks; exhaustive state error-reporting pattern 230 "java:S6541" // Single-threaded session contexts do not require synchronization 231 }) 232 private <T> BeanMap<T> readIntoBeanMap(UonReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException { 233 234 int c = r.peekSkipWs(); 235 if (c == -1) 236 return m; 237 238 // S1: Looking for attrName start. 239 // S2: Found attrName end, looking for =. 240 // S3: Found =, looking for valStart. 241 // S4: Looking for , or } 242 243 boolean isInEscape = false; 244 245 var state = S1; 246 var currAttr = ""; 247 var pb = swapParentBean(m.getBean(false)); 248 mark(); 249 try { 250 while (c != -1) { 251 c = r.read(); 252 if (! isInEscape) { 253 if (state == S1) { 254 if (c == -1) { 255 return m; 256 } 257 r.unread(); 258 mark(); 259 currAttr = readAttrName(r, true); 260 if (currAttr == null) // Value was '%00' 261 return null; 262 state = S2; 263 } else if (state == S2) { 264 if (c == '\u0002') 265 state = S3; 266 else if (c == -1 || c == '\u0001') { 267 m.put(currAttr, null); 268 if (c == -1) 269 return m; 270 state = S1; 271 } 272 } else if (state == S3) { // NOSONAR - State check necessary for state machine 273 if (c == -1 || c == '\u0001') { 274 if (! currAttr.equals(getBeanTypePropertyName((ClassMeta<?>) m.getBeanInfo()))) { 275 var pMeta = m.getPropertyMeta(currAttr); 276 if (pMeta == null) { 277 onUnknownProperty(currAttr, m, null); 278 unmark(); 279 } else { 280 unmark(); 281 setCurrentProperty(pMeta); 282 // In cases of "&foo=", create an empty instance of the value if createable. 283 // Otherwise, leave it null. Arrays (especially primitive arrays 284 // such as byte[]) report canCreateNewInstance()=false because their 285 // element type has no no-arg constructor, but ClassMeta.newInstance() 286 // already handles arrays via Array.newInstance(...,0); fall through 287 // to that path so empty expanded-params arrays round-trip from the 288 // "key=" sentinel emitted by the serializer. 289 var cm = (ClassMeta<?>) pMeta.getBeanInfo(); 290 if (cm.canCreateNewInstance() || cm.isArray()) { 291 try { 292 pMeta.set(m, currAttr, cm.newInstance()); 293 } catch (BeanRuntimeException e) { 294 onBeanSetterException(pMeta, e); 295 throw e; 296 } 297 } 298 setCurrentProperty(null); 299 } 300 } 301 if (c == -1) 302 return m; 303 state = S1; 304 } else { 305 if (! currAttr.equals(getBeanTypePropertyName((ClassMeta<?>) m.getBeanInfo()))) { 306 var pMeta = m.getPropertyMeta(currAttr); 307 if (pMeta == null) { 308 onUnknownProperty(currAttr, m, readAnything(object(), r.unread(), m.getBean(false), true, null)); 309 unmark(); 310 } else { 311 unmark(); 312 setCurrentProperty(pMeta); 313 if (shouldUseExpandedParams(pMeta)) { 314 var et = ((ClassMeta<?>) pMeta.getBeanInfo()).getElementType(); 315 var value = readAnything(et, r.unread(), m.getBean(false), true, pMeta); 316 setName(et, value, currAttr); 317 try { 318 pMeta.add(m, currAttr, value); 319 } catch (BeanRuntimeException e) { 320 onBeanSetterException(pMeta, e); 321 throw e; 322 } 323 } else { 324 var cm = (ClassMeta<?>) pMeta.getBeanInfo(); 325 var value = readAnything(cm, r.unread(), m.getBean(false), true, pMeta); 326 setName(cm, value, currAttr); 327 try { 328 pMeta.set(m, currAttr, value); 329 } catch (BeanRuntimeException e) { 330 onBeanSetterException(pMeta, e); 331 throw e; 332 } 333 } 334 setCurrentProperty(null); 335 } 336 } 337 state = S4; 338 } 339 } else if (state == S4) { 340 if (c == '\u0001') 341 state = S1; 342 else if (c == -1) { 343 return m; 344 } 345 } 346 } 347 isInEscape = (c == '\\' && ! isInEscape); 348 } 349 if (state == S1) 350 throw new ParseException(this, "Could not find attribute name on object."); 351 if (state == S2) 352 throw new ParseException(this, "Could not find '=' following attribute name on object."); 353 if (state == S3) 354 throw new ParseException(this, "Could not find value following '=' on object."); 355 if (state == S4) 356 throw new ParseException(this, "Could not find end of object."); 357 } finally { 358 unmark(); 359 swapParentBean(pb); 360 } 361 362 return null; // Unreachable. 363 } 364 365 private <K,V> Map<K,V> readIntoMap2(UonReader r, Map<K,V> m, ClassMeta<?> type, Object outer) throws IOException, ParseException, ExecutableException { 366 367 var keyType = (ClassMeta<K>)(type.isArgs() || type.isCollectionOrArray() ? getClassMeta(Integer.class) : type.getKeyType()); 368 369 int c = r.peekSkipWs(); 370 if (c == -1) 371 return m; 372 373 // S1: Looking for attrName start. 374 // S2: Found attrName end, looking for =. 375 // S3: Found =, looking for valStart. 376 // S4: Looking for & or end. 377 378 boolean isInEscape = false; 379 380 var state = S1; 381 int argIndex = 0; 382 K currAttr = null; 383 while (c != -1) { 384 c = r.read(); 385 if (! isInEscape) { 386 if (state == S1) { 387 if (c == -1) 388 return m; 389 r.unread(); 390 var attr = readAttr(r, true); 391 currAttr = attr == null ? null : convertAttrToType(m, trim(attr.toString()), keyType); 392 state = S2; 393 c = 0; // Avoid isInEscape if c was '\' 394 } else if (state == S2) { 395 if (c == '\u0002') 396 state = S3; 397 else if (c == -1 || c == '\u0001') { 398 m.put(currAttr, null); 399 if (c == -1) 400 return m; 401 state = S1; 402 } 403 } else if (state == S3) { 404 if (c == -1 || c == '\u0001') { 405 ClassMeta<V> valueType; 406 if (type.isArgs()) { 407 valueType = (ClassMeta<V>)type.getArg(argIndex++); 408 } else if (type.isCollectionOrArray()) { 409 valueType = (ClassMeta<V>)type.getElementType(); 410 } else { 411 valueType = (ClassMeta<V>)type.getValueType(); 412 } 413 V value = convertAttrToType(m, "", valueType); 414 m.put(currAttr, value); 415 if (c == -1) 416 return m; 417 state = S1; 418 } else { 419 // For performance, we bypass readAnything for string values. 420 ClassMeta<V> valueType; 421 if (type.isArgs()) { 422 valueType = (ClassMeta<V>)type.getArg(argIndex++); 423 } else if (type.isCollectionOrArray()) { 424 valueType = (ClassMeta<V>)type.getElementType(); 425 } else { 426 valueType = (ClassMeta<V>)type.getValueType(); 427 } 428 V value = (V)(valueType.isString() ? super.readString(r.unread(), true) : super.readAnything(valueType, r.unread(), outer, true, null)); 429 430 // If we already encountered this parameter, turn it into a list. 431 if (m.containsKey(currAttr) && valueType.isObject()) { 432 Object v2 = m.get(currAttr); 433 if (! (v2 instanceof MarshalledList)) { 434 v2 = new JsonList(v2).setBeanSession(this); 435 m.put(currAttr, (V)v2); 436 } 437 ((MarshalledList)v2).add(value); 438 } else { 439 m.put(currAttr, value); 440 } 441 state = S4; 442 c = 0; // Avoid isInEscape if c was '\' 443 } 444 } else if (state == S4) { 445 if (c == '\u0001') 446 state = S1; 447 else if (c == -1) { 448 return m; 449 } 450 } 451 } 452 isInEscape = (c == '\\' && ! isInEscape); 453 } 454 if (state == S1) 455 throw new ParseException(this, "Could not find attribute name on object."); 456 if (state == S2) 457 throw new ParseException(this, "Could not find '=' following attribute name on object."); 458 if (state == S3) 459 throw new ParseException(this, "Dangling '=' found in object entry"); 460 if (state == S4) 461 throw new ParseException(this, "Could not find end of object."); 462 463 return null; // Unreachable. 464 } 465 466 @Override /* Overridden from ParserSession */ 467 protected <T> T doRead(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException { 468 try (var r = getUonReader(pipe, true)) { 469 return readAnything(type, r, getOuter()); 470 } 471 } 472 473 @Override /* Overridden from ReaderParserSession */ 474 protected <K,V> Map<K,V> doReadIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws Exception { 475 try (var r = getUonReader(pipe, true)) { 476 if (r.peekSkipWs() == '?') { 477 int ignored = r.read(); 478 } 479 m = readIntoMap2(r, m, getClassMeta(Map.class, keyType, valueType), null); 480 return m; 481 } 482 } 483 484 /** 485 * Returns the language-specific metadata on the specified class. 486 * 487 * @param cm The class to return the metadata on. 488 * @return The metadata. 489 */ 490 protected UrlEncodingClassMeta getUrlEncodingClassMeta(ClassMeta<?> cm) { 491 return ctx.getUrlEncodingClassMeta(cm); 492 } 493 494 /** 495 * Routes the {@code _value=...} top-level unwrap through the format-aware 496 * Duration/Period/Date/Calendar/Temporal parsers when the target type is one of those 497 * date/time/duration cluster types. Otherwise falls through to the generic 498 * {@link #convertToType(Object, ClassMeta)} path. 499 * 500 * <p> 501 * Without this routing the inner value (typically a {@link Number} parsed from a bare wire 502 * literal) is converted via the generic {@code Number → T} coercion which does not consult 503 * {@link MarshallingContext#getDurationFormat()} / {@code getTemporalFormat()} etc., so the 504 * configured wire format hint (NANOS, ISO_YEAR, MILLIS, …) is silently dropped. 505 */ 506 private <T> Object unwrapValueAs(Object rawValue, ClassMeta<T> sType) { 507 if (rawValue != null) { 508 if (sType.isDuration()) 509 return readDuration(rawValue.toString()); 510 if (sType.isPeriod()) 511 return readPeriod(rawValue.toString()); 512 if (sType.isDate()) 513 return readDate(rawValue.toString(), sType); 514 if (sType.isCalendar()) 515 return readCalendar(rawValue.toString(), sType); 516 if (sType.isTemporal()) 517 return readTemporal(rawValue.toString(), sType); 518 } 519 return convertToType(rawValue, sType); 520 } 521 522 /** 523 * Parse bean property collections/arrays as separate key/value pairs. 524 * 525 * @see UrlEncodingParser.Builder#expandedParams() 526 * @return 527 * <jk>false</jk> if serializing the array <c>[1,2,3]</c> results in <c>?key=$a(1,2,3)</c>. 528 * <br><jk>true</jk> if serializing the same array results in <c>?key=1&key=2&key=3</c>. 529 */ 530 protected final boolean isExpandedParams() { return expandedParams; } 531}