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 java.util.*;
016import java.util.concurrent.*;
017
018import org.apache.juneau.*;
019import org.apache.juneau.annotation.*;
020import org.apache.juneau.collections.*;
021import org.apache.juneau.http.*;
022import org.apache.juneau.http.header.*;
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 *       .swaps(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 List<MediaType> mediaTypes;
087   private final List<Parser> mediaTypeParsers;
088   private final List<Parser> parsers;
089
090   /**
091    * Instantiates a new clean-slate {@link ParserGroupBuilder} object.
092    *
093    * <p>
094    * This is equivalent to simply calling <code><jk>new</jk> ParserGroupBuilder()</code>.
095    *
096    * @return A new {@link ParserGroupBuilder} object.
097    */
098   public static ParserGroupBuilder create() {
099      return new ParserGroupBuilder();
100   }
101
102   /**
103    * Returns a builder that's a copy of the settings on this parser group.
104    *
105    * @return A new {@link ParserGroupBuilder} initialized to this group.
106    */
107   @Override /* Context */
108   public ParserGroupBuilder builder() {
109      return new ParserGroupBuilder(this);
110   }
111
112   /**
113    * Constructor.
114    *
115    * @param ps
116    *    The modifiable properties that were used to initialize the parsers.
117    *    A snapshot of these will be made so that we can clone and modify this group.
118    * @param parsers
119    *    The parsers defined in this group.
120    *    The order is important because they will be tried in reverse order (e.g. newer first) in which they will be
121    *    tried to match against media types.
122    */
123   public ParserGroup(PropertyStore ps, Parser[] parsers) {
124      super(ps);
125      this.parsers = AList.unmodifiable(parsers);
126
127      AList<MediaType> lmt = AList.of();
128      AList<Parser> l = AList.of();
129      for (Parser p : parsers) {
130         for (MediaType m: p.getMediaTypes()) {
131            lmt.add(m);
132            l.add(p);
133         }
134      }
135
136      this.mediaTypes = lmt.unmodifiable();
137      this.mediaTypeParsers = l.unmodifiable();
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.of(contentTypeHeader);
155      int match = ct.match(mediaTypes);
156
157      if (match >= 0) {
158         pm = new ParserMatch(mediaTypes.get(match), mediaTypeParsers.get(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 mediaTypes;
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
218   /**
219    * Returns <jk>true</jk> if this group contains no parsers.
220    *
221    * @return <jk>true</jk> if this group contains no parsers.
222    */
223   public boolean isEmpty() {
224      return parsers.isEmpty();
225   }
226}