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.parser; 018 019import static java.util.stream.Collectors.*; 020import static org.apache.juneau.common.utils.Utils.*; 021import static org.apache.juneau.internal.CollectionUtils.*; 022 023import java.util.*; 024import java.util.concurrent.*; 025import java.util.function.*; 026import java.util.stream.*; 027 028import org.apache.juneau.*; 029import org.apache.juneau.common.utils.*; 030import org.apache.juneau.cp.*; 031import org.apache.juneau.internal.*; 032import org.apache.juneau.reflect.*; 033 034/** 035 * Represents a group of {@link Parser Parsers} that can be looked up by media type. 036 * 037 * <h5 class='topic'>Description</h5> 038 * 039 * Provides the following features: 040 * <ul class='spaced-list'> 041 * <li> 042 * Finds parsers based on HTTP <c>Content-Type</c> header values. 043 * <li> 044 * Sets common properties on all parsers in a single method call. 045 * <li> 046 * Locks all parsers in a single method call. 047 * <li> 048 * Clones existing groups and all parsers within the group in a single method call. 049 * </ul> 050 * 051 * <h5 class='topic'>Match ordering</h5> 052 * 053 * Parsers are matched against <c>Content-Type</c> strings in the order they exist in this group. 054 * 055 * <p> 056 * Adding new entries will cause the entries to be prepended to the group. 057 * This allows for previous parsers to be overridden through subsequent calls. 058 * 059 * <p> 060 * For example, calling <code>g.append(P1.<jk>class</jk>,P2.<jk>class</jk>).append(P3.<jk>class</jk>,P4.<jk>class</jk>)</code> 061 * will result in the order <c>P3, P4, P1, P2</c>. 062 * 063 * <h5 class='section'>Example:</h5> 064 * <p class='bjava'> 065 * <jc>// Construct a new parser group builder</jc> 066 * ParserSet <jv>parsers</jv> = ParserSet.<jsm>create</jsm>(); 067 * .add(JsonParser.<jk>class</jk>, XmlParser.<jk>class</jk>); <jc>// Add some parsers to it</jc> 068 * .forEach(<jv>x</jv> -> <jv>x</jv>.swaps(CalendarSwap.IsoLocalDateTime.<jk>class</jk>)) 069 * .forEach(<jv>x</jv> -> <jv>x</jv>.beansRequireSerializable()) 070 * .build(); 071 * 072 * <jc>// Find the appropriate parser by Content-Type</jc> 073 * ReaderParser <jv>parser</jv> = (ReaderParser)<jv>parsers</jv>.getParser(<js>"text/json"</js>); 074 * 075 * <jc>// Parse a bean from JSON</jc> 076 * String <jv>json</jv> = <js>"{...}"</js>; 077 * AddressBook <jv>addressBook</jv> = <jv>parser</jv>.parse(<jv>json</jv>, AddressBook.<jk>class</jk>); 078 * </p> 079 * 080 * <h5 class='section'>Notes:</h5><ul> 081 * <li class='note'>This class is thread safe and reusable. 082 * </ul> 083 * 084 * <h5 class='section'>See Also:</h5><ul> 085 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/SerializersAndParsers">Serializers and Parsers</a> 086 087 * </ul> 088 */ 089public class ParserSet { 090 091 //----------------------------------------------------------------------------------------------------------------- 092 // Static 093 //----------------------------------------------------------------------------------------------------------------- 094 095 /** 096 * An identifier that the previous entries in this group should be inherited. 097 * <p> 098 * Used by {@link ParserSet.Builder#set(Class...)} 099 */ 100 @SuppressWarnings("javadoc") 101 public static abstract class Inherit extends Parser { 102 protected Inherit(Parser.Builder builder) { 103 super(builder); 104 } 105 } 106 107 /** 108 * An identifier that the previous entries in this group should not be inherited. 109 * <p> 110 * Used by {@link ParserSet.Builder#add(Class...)} 111 */ 112 @SuppressWarnings("javadoc") 113 public static abstract class NoInherit extends Parser { 114 protected NoInherit(Parser.Builder builder) { 115 super(builder); 116 } 117 } 118 119 /** 120 * Static creator. 121 * 122 * @param beanStore The bean store to use for creating beans. 123 * @return A new builder for this object. 124 */ 125 public static Builder create(BeanStore beanStore) { 126 return new Builder(beanStore); 127 } 128 129 /** 130 * Static creator. 131 * 132 * @return A new builder for this object. 133 */ 134 public static Builder create() { 135 return new Builder(BeanStore.INSTANCE); 136 } 137 138 //----------------------------------------------------------------------------------------------------------------- 139 // Builder 140 //----------------------------------------------------------------------------------------------------------------- 141 142 /** 143 * Builder class. 144 */ 145 public static class Builder extends BeanBuilder<ParserSet> { 146 147 List<Object> entries; 148 private BeanContext.Builder bcBuilder; 149 150 /** 151 * Create an empty parser group builder. 152 * 153 * @param beanStore The bean store to use for creating beans. 154 */ 155 protected Builder(BeanStore beanStore) { 156 super(ParserSet.class, beanStore); 157 this.entries = list(); 158 } 159 160 /** 161 * Clone an existing parser group. 162 * 163 * @param copyFrom The parser group that we're copying settings and parsers from. 164 */ 165 protected Builder(ParserSet copyFrom) { 166 super(copyFrom.getClass(), BeanStore.INSTANCE); 167 this.entries = list((Object[])copyFrom.entries); 168 } 169 170 /** 171 * Clone an existing parser group builder. 172 * 173 * <p> 174 * Parser builders will be cloned during this process. 175 * 176 * @param copyFrom The parser group that we're copying settings and parsers from. 177 */ 178 protected Builder(Builder copyFrom) { 179 super(copyFrom); 180 bcBuilder = copyFrom.bcBuilder == null ? null : copyFrom.bcBuilder.copy(); 181 entries = list(); 182 copyFrom.entries.stream().map(this::copyBuilder).forEach(x -> entries.add(x)); 183 } 184 185 private Object copyBuilder(Object o) { 186 if (o instanceof Parser.Builder) { 187 Parser.Builder x = (Parser.Builder)o; 188 Parser.Builder x2 = x.copy(); 189 if (Utils.ne(x.getClass(), x2.getClass())) 190 throw new BasicRuntimeException("Copy method not implemented on class {0}", x.getClass().getName()); 191 x = x2; 192 if (bcBuilder != null) 193 x.beanContext(bcBuilder); 194 return x; 195 } 196 return o; 197 } 198 199 @Override /* BeanBuilder */ 200 protected ParserSet buildDefault() { 201 return new ParserSet(this); 202 } 203 204 /** 205 * Makes a copy of this builder. 206 * 207 * @return A new copy of this builder. 208 */ 209 public Builder copy() { 210 return new Builder(this); 211 } 212 213 //------------------------------------------------------------------------------------------------------------- 214 // Properties 215 //------------------------------------------------------------------------------------------------------------- 216 217 /** 218 * Associates an existing bean context builder with all parser builders in this group. 219 * 220 * @param value The bean contest builder to associate. 221 * @return This object. 222 */ 223 public Builder beanContext(BeanContext.Builder value) { 224 bcBuilder = value; 225 forEach(x -> x.beanContext(value)); 226 return this; 227 } 228 229 /** 230 * Applies an operation to the bean context builder. 231 * 232 * @param operation The operation to apply. 233 * @return This object. 234 */ 235 public final Builder beanContext(Consumer<BeanContext.Builder> operation) { 236 if (bcBuilder != null) 237 operation.accept(bcBuilder); 238 return this; 239 } 240 241 /** 242 * Adds the specified parsers to this group. 243 * 244 * <p> 245 * Entries are added in-order to the beginning of the list in the group. 246 * 247 * <p> 248 * The {@link NoInherit} class can be used to clear out the existing list of parsers before adding the new entries. 249 * 250 * <h5 class='section'>Example:</h5> 251 * <p class='bjava'> 252 * ParserSet.Builder <jv>builder</jv> = ParserSet.<jsm>create</jsm>(); <jc>// Create an empty builder.</jc> 253 * 254 * <jv>builder</jv>.add(FooParser.<jk>class</jk>); <jc>// Now contains: [FooParser]</jc> 255 * 256 * <jv>builder</jv>.add(BarParser.<jk>class</jk>, BazParser.<jk>class</jk>); <jc>// Now contains: [BarParser,BazParser,FooParser]</jc> 257 * 258 * <jv>builder</jv>.add(NoInherit.<jk>class</jk>, QuxParser.<jk>class</jk>); <jc>// Now contains: [QuxParser]</jc> 259 * </p> 260 * 261 * @param values The parsers to add to this group. 262 * @return This object. 263 * @throws IllegalArgumentException If one or more values do not extend from {@link Parser}. 264 */ 265 public Builder add(Class<?>...values) { 266 List<Object> l = list(); 267 for (Class<?> v : values) 268 if (v.getSimpleName().equals("NoInherit")) 269 clear(); 270 for (Class<?> v : values) { 271 if (Parser.class.isAssignableFrom(v)) { 272 l.add(createBuilder(v)); 273 } else if (! v.getSimpleName().equals("NoInherit")) { 274 throw new BasicRuntimeException("Invalid type passed to ParserSet.Builder.add(): {0}", v.getName()); 275 } 276 } 277 entries.addAll(0, l); 278 return this; 279 } 280 281 /** 282 * Sets the specified parsers for this group. 283 * 284 * <p> 285 * Existing values are overwritten. 286 * 287 * <p> 288 * The {@link Inherit} class can be used to insert existing entries in this group into the position specified. 289 * 290 * <h5 class='section'>Example:</h5> 291 * <p class='bjava'> 292 * ParserSet.Builder <jv>builder</jv> = ParserSet.<jsm>create</jsm>(); <jc>// Create an empty builder.</jc> 293 * 294 * <jv>builder</jv>.set(FooParser.<jk>class</jk>); <jc>// Now contains: [FooParser]</jc> 295 * 296 * <jv>builder</jv>.set(BarParser.<jk>class</jk>, BazParser.<jk>class</jk>); <jc>// Now contains: [BarParser,BazParser]</jc> 297 * 298 * <jv>builder</jv>.set(Inherit.<jk>class</jk>, QuxParser.<jk>class</jk>); <jc>// Now contains: [BarParser,BazParser,QuxParser]</jc> 299 * </p> 300 * 301 * @param values The parsers to set in this group. 302 * @return This object. 303 * @throws IllegalArgumentException If one or more values do not extend from {@link Parser} or named <js>"Inherit"</js>. 304 */ 305 public Builder set(Class<?>...values) { 306 List<Object> l = list(); 307 for (Class<?> v : values) { 308 if (v.getSimpleName().equals("Inherit")) { 309 l.addAll(entries); 310 } else if (Parser.class.isAssignableFrom(v)) { 311 l.add(createBuilder(v)); 312 } else { 313 throw new BasicRuntimeException("Invalid type passed to ParserGrouup.Builder.set(): {0}", v.getName()); 314 } 315 } 316 entries = l; 317 return this; 318 } 319 320 private Object createBuilder(Object o) { 321 if (o instanceof Class) { 322 323 // Check for no-arg constructor. 324 ConstructorInfo ci = ClassInfo.of((Class<?>)o).getPublicConstructor(ConstructorInfo::hasNoParams); 325 if (ci != null) 326 return ci.invoke(); 327 328 // Check for builder. 329 @SuppressWarnings("unchecked") 330 Parser.Builder b = Parser.createParserBuilder((Class<? extends Parser>)o); 331 if (bcBuilder != null) 332 b.beanContext(bcBuilder); 333 o = b; 334 } 335 return o; 336 } 337 338 /** 339 * Registers the specified parsers with this group. 340 * 341 * <p> 342 * When passing in pre-instantiated parsers to this group, applying properties and transforms to the group 343 * do not affect them. 344 * 345 * @param s The parsers to append to this group. 346 * @return This object. 347 */ 348 public Builder add(Parser...s) { 349 prependAll(entries, (Object[])s); 350 return this; 351 } 352 353 /** 354 * Clears out any existing parsers in this group. 355 * 356 * @return This object. 357 */ 358 public Builder clear() { 359 entries.clear(); 360 return this; 361 } 362 363 /** 364 * Returns <jk>true</jk> if at least one of the specified annotations can be applied to at least one parser builder in this group. 365 * 366 * @param work The work to check. 367 * @return <jk>true</jk> if at least one of the specified annotations can be applied to at least one parser builder in this group. 368 */ 369 public boolean canApply(AnnotationWorkList work) { 370 for (Object o : entries) 371 if (o instanceof Parser.Builder) 372 if (((Parser.Builder)o).canApply(work)) 373 return true; 374 return false; 375 } 376 377 /** 378 * Applies the specified annotations to all applicable parser builders in this group. 379 * 380 * @param work The annotations to apply. 381 * @return This object. 382 */ 383 public Builder apply(AnnotationWorkList work) { 384 return forEach(x -> x.apply(work)); 385 } 386 387 /** 388 * Performs an action on all parser builders in this group. 389 * 390 * @param action The action to perform. 391 * @return This object. 392 */ 393 public Builder forEach(Consumer<Parser.Builder> action) { 394 builders(Parser.Builder.class).forEach(action); 395 return this; 396 } 397 398 /** 399 * Performs an action on all writer parser builders in this group. 400 * 401 * @param action The action to perform. 402 * @return This object. 403 */ 404 public Builder forEachRP(Consumer<ReaderParser.Builder> action) { 405 return forEach(ReaderParser.Builder.class, action); 406 } 407 408 /** 409 * Performs an action on all output stream parser builders in this group. 410 * 411 * @param action The action to perform. 412 * @return This object. 413 */ 414 public Builder forEachISP(Consumer<InputStreamParser.Builder> action) { 415 return forEach(InputStreamParser.Builder.class, action); 416 } 417 418 /** 419 * Performs an action on all parser builders of the specified type in this group. 420 * 421 * @param <B> The parser builder type. 422 * @param type The parser builder type. 423 * @param action The action to perform. 424 * @return This object. 425 */ 426 public <B extends Parser.Builder> Builder forEach(Class<B> type, Consumer<B> action) { 427 builders(type).forEach(action); 428 return this; 429 } 430 431 /** 432 * Returns direct access to the {@link Parser} and {@link Parser.Builder} objects in this builder. 433 * 434 * <p> 435 * Provided to allow for any extraneous modifications to the list not accomplishable via other methods on this builder such 436 * as re-ordering/adding/removing entries. 437 * 438 * <p> 439 * Note that it is up to the user to ensure that the list only contains {@link Parser} and {@link Parser.Builder} objects. 440 * 441 * @return The inner list of entries in this builder. 442 */ 443 public List<Object> inner() { 444 return entries; 445 } 446 447 private <T extends Parser.Builder> Stream<T> builders(Class<T> type) { 448 return entries.stream().filter(x -> type.isInstance(x)).map(x -> type.cast(x)); 449 } 450 @Override /* Overridden from BeanBuilder */ 451 public Builder impl(Object value) { 452 super.impl(value); 453 return this; 454 } 455 456 @Override /* Overridden from BeanBuilder */ 457 public Builder type(Class<?> value) { 458 super.type(value); 459 return this; 460 } 461 @Override /* Object */ 462 public String toString() { 463 return entries.stream().map(this::toString).collect(joining(",","[","]")); 464 } 465 466 private String toString(Object o) { 467 if (o == null) 468 return "null"; 469 if (o instanceof Parser.Builder) 470 return "builder:" + o.getClass().getName(); 471 return "parser:" + o.getClass().getName(); 472 } 473 } 474 475 //----------------------------------------------------------------------------------------------------------------- 476 // Instance 477 //----------------------------------------------------------------------------------------------------------------- 478 479 // Maps Content-Type headers to matches. 480 private final ConcurrentHashMap<String,ParserMatch> cache = new ConcurrentHashMap<>(); 481 482 private final MediaType[] mediaTypes; 483 private final Parser[] mediaTypeParsers; 484 485 final Parser[] entries; 486 487 /** 488 * Constructor. 489 * 490 * @param builder The builder for this bean. 491 */ 492 public ParserSet(Builder builder) { 493 494 this.entries = builder.entries.stream().map(this::build).toArray(Parser[]::new); 495 496 List<MediaType> lmt = list(); 497 List<Parser> l = list(); 498 for (Parser e : entries) { 499 e.getMediaTypes().forEach(x -> { 500 lmt.add(x); 501 l.add(e); 502 }); 503 } 504 505 this.mediaTypes = Utils.array(lmt, MediaType.class); 506 this.mediaTypeParsers = Utils.array(l, Parser.class); 507 } 508 509 private Parser build(Object o) { 510 if (o instanceof Parser) 511 return (Parser)o; 512 return ((Parser.Builder)o).build(); 513 } 514 515 /** 516 * Creates a copy of this parser group. 517 * 518 * @return A new copy of this parser group. 519 */ 520 public Builder copy() { 521 return new Builder(this); 522 } 523 524 /** 525 * Searches the group for a parser that can handle the specified <l>Content-Type</l> header value. 526 * 527 * <p> 528 * The returned object includes both the parser and media type that matched. 529 * 530 * @param contentTypeHeader The HTTP <l>Content-Type</l> header value. 531 * @return The parser and media type that matched the content type header, or <jk>null</jk> if no match was made. 532 */ 533 public ParserMatch getParserMatch(String contentTypeHeader) { 534 ParserMatch pm = cache.get(contentTypeHeader); 535 if (pm != null) 536 return pm; 537 538 MediaType ct = MediaType.of(contentTypeHeader); 539 int match = ct.match(alist(mediaTypes)); 540 541 if (match >= 0) { 542 pm = new ParserMatch(mediaTypes[match], mediaTypeParsers[match]); 543 cache.putIfAbsent(contentTypeHeader, pm); 544 } 545 546 return cache.get(contentTypeHeader); 547 } 548 549 /** 550 * Same as {@link #getParserMatch(String)} but matches using a {@link MediaType} instance. 551 * 552 * @param mediaType The HTTP <l>Content-Type</l> header value as a media type. 553 * @return The parser and media type that matched the media type, or <jk>null</jk> if no match was made. 554 */ 555 public ParserMatch getParserMatch(MediaType mediaType) { 556 return getParserMatch(mediaType.toString()); 557 } 558 559 /** 560 * Same as {@link #getParserMatch(String)} but returns just the matched parser. 561 * 562 * @param contentTypeHeader The HTTP <l>Content-Type</l> header string. 563 * @return The parser that matched the content type header, or <jk>null</jk> if no match was made. 564 */ 565 public Parser getParser(String contentTypeHeader) { 566 ParserMatch pm = getParserMatch(contentTypeHeader); 567 return pm == null ? null : pm.getParser(); 568 } 569 570 /** 571 * Same as {@link #getParserMatch(MediaType)} but returns just the matched parser. 572 * 573 * @param mediaType The HTTP media type. 574 * @return The parser that matched the media type, or <jk>null</jk> if no match was made. 575 */ 576 public Parser getParser(MediaType mediaType) { 577 ParserMatch pm = getParserMatch(mediaType); 578 return pm == null ? null : pm.getParser(); 579 } 580 581 /** 582 * Returns the media types that all parsers in this group can handle 583 * 584 * <p> 585 * Entries are ordered in the same order as the parsers in the group. 586 * 587 * @return An unmodifiable list of media types. 588 */ 589 public List<MediaType> getSupportedMediaTypes() { 590 return u(alist(mediaTypes)); 591 } 592 593 /** 594 * Returns the parsers in this group. 595 * 596 * @return An unmodifiable list of parsers in this group. 597 */ 598 public List<Parser> getParsers() { 599 return u(alist(entries)); 600 } 601 602 /** 603 * Returns <jk>true</jk> if this group contains no parsers. 604 * 605 * @return <jk>true</jk> if this group contains no parsers. 606 */ 607 public boolean isEmpty() { 608 return entries.length == 0; 609 } 610}