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.uon; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020import static org.apache.juneau.commons.utils.SystemUtils.*; 021 022import java.io.*; 023import java.lang.reflect.*; 024import java.util.*; 025import java.util.concurrent.*; 026 027import org.apache.juneau.commons.bean.*; 028import org.apache.juneau.commons.collections.*; 029import org.apache.juneau.commons.httppart.*; 030import org.apache.juneau.marshall.*; 031import org.apache.juneau.marshall.httppart.*; 032import org.apache.juneau.marshall.parser.*; 033import org.apache.juneau.marshall.stream.*; 034 035/** 036 * Parses UON (a notation for URL-encoded query parameter values) text into POJO models. 037 * 038 * <h5 class='topic'>Media types</h5> 039 * <p> 040 * Handles <c>Content-Type</c> types: <bc>text/uon</bc> 041 * 042 * <h5 class='topic'>Description</h5> 043 * <p> 044 * This parser uses a state machine, which makes it very fast and efficient. 045 * 046 * <h5 class='figure'>Example input (Map of name/age):</h5> 047 * <p class='bcode'> 048 * (name=Alice,age=30) 049 * </p> 050 * 051 * <h5 class='figure'>Complex (nested object + array):</h5> 052 * <p class='bcode'> 053 * (name=Alice,age=30,address=(street=123+Main+St,city=Boston,state=MA),tags=@(a,b,c)) 054 * </p> 055 * 056 * <h5 class='section'>Notes:</h5><ul> 057 * <li class='note'>This class is thread safe and reusable. 058 * </ul> 059 * 060 * <h5 class='section'>See Also:</h5><ul> 061 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/UonSupport">UON</a> 062 063 * </ul> 064 */ 065@SuppressWarnings({ 066 "java:S110", // UonParser extends ReaderParser on the shared Parser base-class chain, and UonParser.Decoding subclasses it; the depth comes from that format-family design. 067 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 068 "java:S9149", // Per-format static factories intentionally shadow the parent's. 069 "resource" // Closeable resources are owned by the caller's parser session; Eclipse JDT @Owning warning is by design. 070}) 071public class UonParser extends ReaderParser implements HttpPartParser, UonMetaProvider, RecordReadable { 072 073 /** 074 * Builder class. 075 */ 076 @SuppressWarnings({ 077 "java:S119" // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name. 078 }) 079 public abstract static class Builder<SELF extends Builder<SELF>> extends ReaderParser.Builder<SELF> { 080 081 private static final Cache<HashKey,UonParser> CACHE = Cache.of(HashKey.class, UonParser.class).build(); 082 083 private boolean decoding; 084 private boolean validateEnd; 085 086 /** 087 * Constructor, default settings. 088 */ 089 protected Builder() { 090 consumes("text/uon"); 091 decoding = env("UonParser.decoding", false); 092 validateEnd = env("UonParser.validateEnd", false); 093 } 094 095 /** 096 * Copy constructor. 097 * 098 * @param copyFrom The builder to copy from. 099 * <br>Cannot be <jk>null</jk>. 100 */ 101 protected Builder(Builder<?> copyFrom) { 102 super(reqnn("copyFrom", copyFrom)); 103 decoding = copyFrom.decoding; 104 validateEnd = copyFrom.validateEnd; 105 } 106 107 /** 108 * Copy constructor. 109 * 110 * @param copyFrom The bean to copy from. 111 * <br>Cannot be <jk>null</jk>. 112 */ 113 protected Builder(UonParser copyFrom) { 114 super(reqnn("copyFrom", copyFrom)); 115 decoding = copyFrom.decoding; 116 validateEnd = copyFrom.validateEnd; 117 } 118 119 @Override /* Overridden from Context.Builder<?> */ 120 public UonParser build() { 121 return cache(CACHE).build(UonParser.class); 122 } 123 124 @Override /* Overridden from Context.Builder<?> */ 125 public abstract SELF copy(); 126 127 /** 128 * Decode <js>"%xx"</js> sequences. 129 * 130 * <p> 131 * When enabled, URI encoded characters will be decoded. Otherwise it's assumed that they've already been decoded 132 * before being passed to this parser. 133 * 134 * <h5 class='section'>Example:</h5> 135 * <p class='bjava'> 136 * <jc>// Create a decoding UON parser.</jc> 137 * ReaderParser <jv>parser</jv> = UonParser. 138 * .<jsm>create</jsm>() 139 * .decoding() 140 * .build(); 141 * 142 * <jc>// Produces: ["foo bar", "baz quz"].</jc> 143 * String[] <jv>foo</jv> = <jv>parser</jv>.read(<js>"@(foo%20bar,baz%20qux)"</js>, String[].<jk>class</jk>); 144 * </p> 145 * 146 * @return This object. 147 */ 148 public SELF decoding() { 149 return decoding(true); 150 } 151 152 /** 153 * Same as {@link #decoding()} but allows you to explicitly specify the value. 154 * 155 * @param value The value for this setting. 156 * @return This object. 157 */ 158 public SELF decoding(boolean value) { 159 decoding = value; 160 return self(); 161 } 162 163 @Override /* Overridden from Context.Builder<?> */ 164 public HashKey hashKey() { 165 // @formatter:off 166 return HashKey.of( 167 super.hashKey(), 168 decoding, 169 validateEnd 170 ); 171 // @formatter:on 172 } 173 174 175 /** 176 * Validate end. 177 * 178 * <p> 179 * When enabled, after parsing a POJO from the input, verifies that the remaining input in 180 * the stream consists of only comments or whitespace. 181 * 182 * <h5 class='section'>Example:</h5> 183 * <p class='bjava'> 184 * <jc>// Create a parser using strict mode.</jc> 185 * ReaderParser <jv>parser</jv> = UonParser. 186 * .<jsm>create</jsm>() 187 * .validateEnd() 188 * .build(); 189 * 190 * <jc>// Should fail because input has multiple POJOs.</jc> 191 * String <jv>in</jv> = <js>"(foo=bar)(baz=qux)"</js>; 192 * MyBean <jv>myBean</jv> = <jv>parser</jv>.read(<jv>in</jv>, MyBean.<jk>class</jk>); 193 * </p> 194 * 195 * @return This object. 196 */ 197 public SELF validateEnd() { 198 return validateEnd(true); 199 } 200 201 /** 202 * Same as {@link #validateEnd()} but allows you to explicitly specify the value. 203 * 204 * @param value The value for this setting. 205 * @return This object. 206 */ 207 public SELF validateEnd(boolean value) { 208 validateEnd = value; 209 return self(); 210 } 211 } 212 213 /** 214 * Concrete default builder leaf for the non-subclassed {@link UonParser#create()} / {@link UonParser#copy()} path. 215 */ 216 public static final class DefaultBuilder extends Builder<DefaultBuilder> { 217 218 DefaultBuilder() {} 219 220 DefaultBuilder(UonParser copyFrom) { 221 super(copyFrom); 222 } 223 224 DefaultBuilder(Builder<?> copyFrom) { 225 super(copyFrom); 226 } 227 228 @Override /* Overridden from Context.Builder<?> */ 229 public DefaultBuilder copy() { 230 return new DefaultBuilder(this); 231 } 232 } 233 234 /** Default parser, decoding. */ 235 public static class Decoding extends UonParser { 236 237 /** 238 * Constructor. 239 * 240 * @param builder The builder for this object. 241 */ 242 public Decoding(Builder<?> builder) { 243 super(builder.decoding()); 244 } 245 } 246 247 /** Reusable instance of {@link UonParser}, all default settings. */ 248 public static final UonParser DEFAULT = new UonParser(create()); 249 /** Reusable instance of {@link UonParser} with decodeChars set to true. */ 250 public static final UonParser DEFAULT_DECODING = new UonParser.Decoding(create()); 251 252 /** 253 * Creates a new builder for this object. 254 * 255 * @return A new builder. 256 */ 257 @SuppressWarnings({ 258 "java:S1452" // Builder<?> wildcard return intentional; callers chain via fluent API without needing the concrete type 259 }) 260 public static Builder<?> create() { 261 return new DefaultBuilder(); 262 } 263 264 protected final boolean decoding; 265 protected final boolean validateEnd; 266 267 private final Map<BeanPropertyMeta,UonBeanPropertyMeta> uonBeanPropertyMetas = new ConcurrentHashMap<>(); 268 private final Map<ClassMeta<?>,UonClassMeta> uonClassMetas = new ConcurrentHashMap<>(); 269 270 /** 271 * Constructor. 272 * 273 * @param builder The builder for this object. 274 */ 275 public UonParser(Builder<?> builder) { 276 super(builder); 277 decoding = builder.decoding; 278 validateEnd = builder.validateEnd; 279 } 280 281 @Override /* Overridden from Context */ 282 public Builder<?> copy() { 283 return new DefaultBuilder(this); 284 } 285 286 @Override /* Overridden from Context */ 287 public UonParserSession.Builder<?> createSession() { 288 return UonParserSession.create(this); 289 } 290 291 @Override 292 public <T> ClassMeta<T> getClassMeta(Class<T> c) { 293 return getMarshallingContext().getClassMeta(c); 294 } 295 296 @Override 297 public <T> ClassMeta<T> getClassMeta(Type t, Type...args) { 298 return getMarshallingContext().getClassMeta(t, args); 299 } 300 301 @Override /* Overridden from HttpPartParser */ 302 public UonParserSession getPartSession() { return UonParserSession.create(this).build(); } 303 304 @Override /* Overridden from Context */ 305 public UonParserSession getSession() { return createSession().build(); } 306 307 /** 308 * Convenience delegator that opens a whole-value {@link RecordReader} over the input using 309 * <b>default session arguments</b> (mirrors {@link #read(Object, Class)}). 310 * 311 * <p> 312 * The real implementation lives on {@link UonParserSession#readRecords(Object)}. Callers 313 * that need request-derived configuration (locale, timezone, schema, swaps) should call 314 * {@link #createSession()} and invoke {@link UonParserSession#readRecords(Object)} on the 315 * built session instead. 316 * 317 * @param input The input. 318 * @return A new {@link RecordReader} cursor. 319 * @throws IOException If a problem occurred opening the underlying input. 320 */ 321 @Override /* RecordReadable */ 322 public RecordReader readRecords(Object input) throws IOException { 323 return getSession().readRecords(input); 324 } 325 326 @Override /* RecordReadable */ 327 public boolean isRecordStreaming() { 328 return false; 329 } 330 331 @Override /* Overridden from UonMetaProvider */ 332 public UonBeanPropertyMeta getUonBeanPropertyMeta(BeanPropertyMeta bpm) { 333 if (bpm == null) 334 return UonBeanPropertyMeta.DEFAULT; 335 return uonBeanPropertyMetas.computeIfAbsent(bpm, k -> new UonBeanPropertyMeta(k.getDelegateFor(), this)); 336 } 337 338 @Override /* Overridden from UonMetaProvider */ 339 public UonClassMeta getUonClassMeta(ClassMeta<?> cm) { 340 return uonClassMetas.computeIfAbsent(cm, k -> new UonClassMeta(k, this)); 341 } 342 343 /** 344 * Converts the specified input to the specified class type. 345 * 346 * @param <T> The POJO type to transform the input into. 347 * @param partType The part type being parsed. 348 * @param schema 349 * Schema information about the part. 350 * <br>May be <jk>null</jk>. 351 * <br>Not all part parsers use the schema information. 352 * @param in The input being parsed. 353 * @param toType The POJO type to transform the input into. 354 * @return The parsed value. 355 * @throws ParseException Malformed input encountered. 356 * @throws SchemaValidationException If the input or resulting HTTP part object fails schema validation. 357 */ 358 public <T> T read(HttpPartType partType, HttpPartSchema schema, String in, Class<T> toType) throws ParseException, SchemaValidationException { 359 return getPartSession().read(partType, schema, in, getClassMeta(toType)); 360 } 361 362 /** 363 * Converts the specified input to the specified class type. 364 * 365 * @param <T> The POJO type to transform the input into. 366 * @param partType The part type being parsed. 367 * @param schema 368 * Schema information about the part. 369 * <br>May be <jk>null</jk>. 370 * <br>Not all part parsers use the schema information. 371 * @param in The input being parsed. 372 * @param toType The POJO type to transform the input into. 373 * @return The parsed value. 374 * @throws ParseException Malformed input encountered. 375 * @throws SchemaValidationException If the input or resulting HTTP part object fails schema validation. 376 */ 377 public <T> T read(HttpPartType partType, HttpPartSchema schema, String in, ClassMeta<T> toType) throws ParseException, SchemaValidationException { 378 return getPartSession().read(partType, schema, in, toType); 379 } 380 381 /** 382 * Converts the specified input to the specified class type. 383 * 384 * @param <T> The POJO type to transform the input into. 385 * @param partType The part type being parsed. 386 * @param schema 387 * Schema information about the part. 388 * <br>May be <jk>null</jk>. 389 * <br>Not all part parsers use the schema information. 390 * @param in The input being parsed. 391 * @param toType The POJO type to transform the input into. 392 * @param toTypeArgs The generic type arguments of the POJO type to transform the input into. 393 * @return The parsed value. 394 * @throws ParseException Malformed input encountered. 395 * @throws SchemaValidationException If the input or resulting HTTP part object fails schema validation. 396 */ 397 public <T> T read(HttpPartType partType, HttpPartSchema schema, String in, Type toType, Type...toTypeArgs) throws ParseException, SchemaValidationException { 398 return getPartSession().read(partType, schema, in, getClassMeta(toType, toTypeArgs)); 399 } 400 401 /** 402 * Decode <js>"%xx"</js> sequences enabled 403 * 404 * @see Builder#decoding() 405 * @return 406 * <jk>true</jk> if URI encoded characters should be decoded, <jk>false</jk> if they've already been decoded 407 * before being passed to this parser. 408 */ 409 protected final boolean isDecoding() { return decoding; } 410 411 /** 412 * Validate end enabled. 413 * 414 * @see Builder#validateEnd() 415 * @return 416 * <jk>true</jk> if after parsing a POJO from the input, verifies that the remaining input in 417 * the stream consists of only comments or whitespace. 418 */ 419 protected final boolean isValidateEnd() { return validateEnd; } 420 421 @Override /* Overridden from ReaderParser */ 422 protected FluentMap<String,Object> properties() { 423 return super.properties() 424 .a("decoding", decoding) 425 .a("validateEnd", validateEnd); 426 } 427}