001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.parser;
014
015import static org.apache.juneau.internal.CollectionUtils.*;
016
017import java.util.*;
018import java.util.concurrent.*;
019
020import org.apache.juneau.*;
021import org.apache.juneau.annotation.*;
022import org.apache.juneau.http.*;
023
024/**
025 * Represents a group of {@link Parser Parsers} that can be looked up by media type.
026 *
027 * <h5 class='topic'>Description</h5>
028 *
029 * Provides the following features:
030 * <ul class='spaced-list'>
031 *    <li>
032 *       Finds parsers based on HTTP <c>Content-Type</c> header values.
033 *    <li>
034 *       Sets common properties on all parsers in a single method call.
035 *    <li>
036 *       Locks all parsers in a single method call.
037 *    <li>
038 *       Clones existing groups and all parsers within the group in a single method call.
039 * </ul>
040 *
041 * <h5 class='topic'>Match ordering</h5>
042 *
043 * Parsers are matched against <c>Content-Type</c> strings in the order they exist in this group.
044 *
045 * <p>
046 * Adding new entries will cause the entries to be prepended to the group.
047 * This allows for previous parsers to be overridden through subsequent calls.
048 *
049 * <p>
050 * For example, calling <code>g.append(P1.<jk>class</jk>,P2.<jk>class</jk>).append(P3.<jk>class</jk>,P4.<jk>class</jk>)</code>
051 * will result in the order <c>P3, P4, P1, P2</c>.
052 *
053 * <h5 class='section'>Example:</h5>
054 * <p class='bcode w800'>
055 *    <jc>// Construct a new parser group builder</jc>
056 *    ParserGroupBuilder b = ParserGroup.<jsm>create</jsm>();
057 *
058 *    <jc>// Add some parsers to it</jc>
059 *    b.append(JsonParser.<jk>class</jk>, XmlParser.<jk>class</jk>);
060 *
061 *    <jc>// Change settings on parsers simultaneously</jc>
062 *    b.set(BeanContext.<jsf>BEAN_beansRequireSerializable</jsf>, <jk>true</jk>)
063 *       .pojoSwaps(TemporalCalendarSwap.IsoLocalDateTime.<jk>class</jk>);
064 *
065 *    ParserGroup g = b.build();
066 *
067 *    <jc>// Find the appropriate parser by Content-Type</jc>
068 *    ReaderParser p = (ReaderParser)g.getParser(<js>"text/json"</js>);
069 *
070 *    <jc>// Parse a bean from JSON</jc>
071 *    String json = <js>"{...}"</js>;
072 *    AddressBook addressBook = p.parse(json, AddressBook.<jk>class</jk>);
073 * </p>
074 */
075@ConfigurableContext(nocache=true)
076public final class ParserGroup extends BeanContext {
077
078   /**
079    * An unmodifiable empty parser group.
080    */
081   public static final ParserGroup EMPTY = create().build();
082
083   // Maps Content-Type headers to matches.
084   private final ConcurrentHashMap<String,ParserMatch> cache = new ConcurrentHashMap<>();
085
086   private final MediaType[] mediaTypes;            // List of media types
087   private final List<MediaType> mediaTypesList;
088   private final Parser[] mediaTypeParsers;
089   private final List<Parser> parsers;
090
091   /**
092    * Instantiates a new clean-slate {@link ParserGroupBuilder} object.
093    *
094    * <p>
095    * This is equivalent to simply calling <code><jk>new</jk> ParserGroupBuilder()</code>.
096    *
097    * @return A new {@link ParserGroupBuilder} object.
098    */
099   public static ParserGroupBuilder create() {
100      return new ParserGroupBuilder();
101   }
102
103   /**
104    * Returns a builder that's a copy of the settings on this parser group.
105    *
106    * @return A new {@link ParserGroupBuilder} initialized to this group.
107    */
108   @Override /* Context */
109   public ParserGroupBuilder builder() {
110      return new ParserGroupBuilder(this);
111   }
112
113   /**
114    * Constructor.
115    *
116    * @param ps
117    *    The modifiable properties that were used to initialize the parsers.
118    *    A snapshot of these will be made so that we can clone and modify this group.
119    * @param parsers
120    *    The parsers defined in this group.
121    *    The order is important because they will be tried in reverse order (e.g. newer first) in which they will be
122    *    tried to match against media types.
123    */
124   public ParserGroup(PropertyStore ps, Parser[] parsers) {
125      super(ps);
126      this.parsers = immutableList(parsers);
127
128      List<MediaType> lmt = new ArrayList<>();
129      List<Parser> l = new ArrayList<>();
130      for (Parser p : parsers) {
131         for (MediaType m: p.getMediaTypes()) {
132            lmt.add(m);
133            l.add(p);
134         }
135      }
136
137      this.mediaTypes = lmt.toArray(new MediaType[lmt.size()]);
138      this.mediaTypesList = unmodifiableList(lmt);
139      this.mediaTypeParsers = l.toArray(new Parser[l.size()]);
140   }
141
142   /**
143    * Searches the group for a parser that can handle the specified <l>Content-Type</l> header value.
144    *
145    * <p>
146    * The returned object includes both the parser and media type that matched.
147    *
148    * @param contentTypeHeader The HTTP <l>Content-Type</l> header value.
149    * @return The parser and media type that matched the content type header, or <jk>null</jk> if no match was made.
150    */
151   public ParserMatch getParserMatch(String contentTypeHeader) {
152      ParserMatch pm = cache.get(contentTypeHeader);
153      if (pm != null)
154         return pm;
155
156      ContentType ct = ContentType.forString(contentTypeHeader);
157      int match = ct.findMatch(mediaTypes);
158
159      if (match >= 0) {
160         pm = new ParserMatch(mediaTypes[match], mediaTypeParsers[match]);
161         cache.putIfAbsent(contentTypeHeader, pm);
162      }
163
164      return cache.get(contentTypeHeader);
165   }
166
167   /**
168    * Same as {@link #getParserMatch(String)} but matches using a {@link MediaType} instance.
169    *
170    * @param mediaType The HTTP <l>Content-Type</l> header value as a media type.
171    * @return The parser and media type that matched the media type, or <jk>null</jk> if no match was made.
172    */
173   public ParserMatch getParserMatch(MediaType mediaType) {
174      return getParserMatch(mediaType.toString());
175   }
176
177   /**
178    * Same as {@link #getParserMatch(String)} but returns just the matched parser.
179    *
180    * @param contentTypeHeader The HTTP <l>Content-Type</l> header string.
181    * @return The parser that matched the content type header, or <jk>null</jk> if no match was made.
182    */
183   public Parser getParser(String contentTypeHeader) {
184      ParserMatch pm = getParserMatch(contentTypeHeader);
185      return pm == null ? null : pm.getParser();
186   }
187
188   /**
189    * Same as {@link #getParserMatch(MediaType)} but returns just the matched parser.
190    *
191    * @param mediaType The HTTP media type.
192    * @return The parser that matched the media type, or <jk>null</jk> if no match was made.
193    */
194   public Parser getParser(MediaType mediaType) {
195      ParserMatch pm = getParserMatch(mediaType);
196      return pm == null ? null : pm.getParser();
197   }
198
199   /**
200    * Returns the media types that all parsers in this group can handle
201    *
202    * <p>
203    * Entries are ordered in the same order as the parsers in the group.
204    *
205    * @return An unmodifiable list of media types.
206    */
207   public List<MediaType> getSupportedMediaTypes() {
208      return mediaTypesList;
209   }
210
211   /**
212    * Returns the parsers in this group.
213    *
214    * @return An unmodifiable list of parsers in this group.
215    */
216   public List<Parser> getParsers() {
217      return parsers;
218   }
219}