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'>
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   // Maps Content-Type headers to matches.
077   private final ConcurrentHashMap<String,ParserMatch> cache = new ConcurrentHashMap<>();
078
079   private final MediaType[] mediaTypes;            // List of media types
080   private final List<MediaType> mediaTypesList;
081   private final Parser[] mediaTypeParsers;
082   private final List<Parser> parsers;
083
084   /**
085    * Instantiates a new clean-slate {@link ParserGroupBuilder} object.
086    * 
087    * <p>
088    * This is equivalent to simply calling <code><jk>new</jk> ParserGroupBuilder()</code>.
089    * 
090    * @return A new {@link ParserGroupBuilder} object.
091    */
092   public static ParserGroupBuilder create() {
093      return new ParserGroupBuilder();
094   }
095   
096   /**
097    * Returns a builder that's a copy of the settings on this parser group.
098    * 
099    * @return A new {@link ParserGroupBuilder} initialized to this group.
100    */
101   @Override /* Context */
102   public ParserGroupBuilder builder() {
103      return new ParserGroupBuilder(this);
104   }
105   
106   /**
107    * Constructor.
108    * 
109    * @param ps
110    *    The modifiable properties that were used to initialize the parsers.
111    *    A snapshot of these will be made so that we can clone and modify this group.
112    * @param parsers
113    *    The parsers defined in this group.
114    *    The order is important because they will be tried in reverse order (e.g. newer first) in which they will be
115    *    tried to match against media types.
116    */
117   public ParserGroup(PropertyStore ps, Parser[] parsers) {
118      super(ps);
119      this.parsers = immutableList(parsers);
120
121      List<MediaType> lmt = new ArrayList<>();
122      List<Parser> l = new ArrayList<>();
123      for (Parser p : parsers) {
124         for (MediaType m: p.getMediaTypes()) {
125            lmt.add(m);
126            l.add(p);
127         }
128      }
129
130      this.mediaTypes = lmt.toArray(new MediaType[lmt.size()]);
131      this.mediaTypesList = unmodifiableList(lmt);
132      this.mediaTypeParsers = l.toArray(new Parser[l.size()]);
133   }
134
135   /**
136    * Searches the group for a parser that can handle the specified <l>Content-Type</l> header value.
137    * 
138    * <p>
139    * The returned object includes both the parser and media type that matched.
140    * 
141    * @param contentTypeHeader The HTTP <l>Content-Type</l> header value.
142    * @return The parser and media type that matched the content type header, or <jk>null</jk> if no match was made.
143    */
144   public ParserMatch getParserMatch(String contentTypeHeader) {
145      ParserMatch pm = cache.get(contentTypeHeader);
146      if (pm != null)
147         return pm;
148
149      ContentType ct = ContentType.forString(contentTypeHeader);
150      int match = ct.findMatch(mediaTypes);
151
152      if (match >= 0) {
153         pm = new ParserMatch(mediaTypes[match], mediaTypeParsers[match]);
154         cache.putIfAbsent(contentTypeHeader, pm);
155      }
156
157      return cache.get(contentTypeHeader);
158   }
159
160   /**
161    * Same as {@link #getParserMatch(String)} but matches using a {@link MediaType} instance.
162    * 
163    * @param mediaType The HTTP <l>Content-Type</l> header value as a media type.
164    * @return The parser and media type that matched the media type, or <jk>null</jk> if no match was made.
165    */
166   public ParserMatch getParserMatch(MediaType mediaType) {
167      return getParserMatch(mediaType.toString());
168   }
169
170   /**
171    * Same as {@link #getParserMatch(String)} but returns just the matched parser.
172    * 
173    * @param contentTypeHeader The HTTP <l>Content-Type</l> header string.
174    * @return The parser that matched the content type header, or <jk>null</jk> if no match was made.
175    */
176   public Parser getParser(String contentTypeHeader) {
177      ParserMatch pm = getParserMatch(contentTypeHeader);
178      return pm == null ? null : pm.getParser();
179   }
180
181   /**
182    * Same as {@link #getParserMatch(MediaType)} but returns just the matched parser.
183    * 
184    * @param mediaType The HTTP media type.
185    * @return The parser that matched the media type, or <jk>null</jk> if no match was made.
186    */
187   public Parser getParser(MediaType mediaType) {
188      ParserMatch pm = getParserMatch(mediaType);
189      return pm == null ? null : pm.getParser();
190   }
191
192   /**
193    * Returns the media types that all parsers in this group can handle
194    * 
195    * <p>
196    * Entries are ordered in the same order as the parsers in the group.
197    * 
198    * @return An unmodifiable list of media types.
199    */
200   public List<MediaType> getSupportedMediaTypes() {
201      return mediaTypesList;
202   }
203
204   /**
205    * Returns the parsers in this group.
206    * 
207    * @return An unmodifiable list of parsers in this group.
208    */
209   public List<Parser> getParsers() {
210      return parsers;
211   }
212}