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> -&gt; <jv>x</jv>.swaps(CalendarSwap.IsoLocalDateTime.<jk>class</jk>))
069 *    .forEach(<jv>x</jv> -&gt; <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}