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