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.serializer;
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 Serializer Serializers} 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 serializers based on HTTP <code>Accept</code> header values.
032 *    <li>
033 *       Sets common properties on all serializers in a single method call.
034 *    <li>
035 *       Clones existing groups and all serializers within the group in a single method call.
036 * </ul>
037 *
038 * <h5 class='topic'>Match ordering</h5>
039 *
040 * Serializers are matched against <code>Accept</code> strings in the order they exist in this group.
041 *
042 * <p>
043 * Adding new entries will cause the entries to be prepended to the group.
044 * This allows for previous serializers to be overridden through subsequent calls.
045 *
046 * <p>
047 * For example, calling <code>g.append(S1.<jk>class</jk>,S2.<jk>class</jk>).append(S3.<jk>class</jk>,S4.<jk>class</jk>)</code>
048 * will result in the order <code>S3, S4, S1, S2</code>.
049 *
050 * <h5 class='section'>Example:</h5>
051 * <p class='bcode w800'>
052 *    <jc>// Construct a new serializer group</jc>
053 *    SerializerGroup g = SerializerGroup.<jsm>create</jsm>();
054 *       .append(JsonSerializer.<jk>class</jk>, XmlSerializer.<jk>class</jk>); <jc>// Add some serializers to it</jc>
055 *       .ws().pojoSwaps(CalendarSwap.ISO8601DT.<jk>class</jk>) <jc>// Change settings for all serializers in the group.</jc>
056 *       .build();
057 *
058 *    <jc>// Find the appropriate serializer by Accept type</jc>
059 *    WriterSerializer s = g.getWriterSerializer(<js>"text/foo, text/json;q=0.8, text/*;q:0.6, *\/*;q=0.0"</js>);
060 *
061 *    <jc>// Serialize a bean to JSON text </jc>
062 *    AddressBook addressBook = <jk>new</jk> AddressBook();  <jc>// Bean to serialize.</jc>
063 *    String json = s.serialize(addressBook);
064 * </p>
065 */
066public final class SerializerGroup extends BeanTraverseContext {
067
068   /**
069    * An unmodifiable empty serializer group.
070    */
071   public static final SerializerGroup EMPTY = create().build();
072
073   // Maps Accept headers to matching serializers.
074   private final ConcurrentHashMap<String,SerializerMatch> cache = new ConcurrentHashMap<>();
075
076   private final MediaTypeRange[] mediaTypeRanges;
077   private final Serializer[] mediaTypeRangeSerializers;
078
079   private final List<MediaType> mediaTypesList;
080   private final List<Serializer> serializers;
081
082   /**
083    * Instantiates a new clean-slate {@link SerializerGroupBuilder} object.
084    *
085    * <p>
086    * This is equivalent to simply calling <code><jk>new</jk> SerializerGroupBuilder()</code>.
087    *
088    * @return A new {@link SerializerGroupBuilder} object.
089    */
090   public static SerializerGroupBuilder create() {
091      return new SerializerGroupBuilder();
092   }
093
094   /**
095    * Returns a builder that's a copy of the settings on this serializer group.
096    *
097    * @return A new {@link SerializerGroupBuilder} initialized to this group.
098    */
099   @Override /* Context */
100   public SerializerGroupBuilder builder() {
101      return new SerializerGroupBuilder(this);
102   }
103
104   /**
105    * Constructor.
106    *
107    * @param ps
108    *    The modifiable properties that were used to initialize the serializers.
109    *    A snapshot of these will be made so that we can clone and modify this group.
110    * @param serializers
111    *    The serializers defined in this group.
112    *    The order is important because they will be tried in reverse order (e.g.newer first) in which they will be tried
113    *    to match against media types.
114    */
115   public SerializerGroup(PropertyStore ps, Serializer[] serializers) {
116      super(ps);
117      this.serializers = immutableList(serializers);
118
119      List<MediaTypeRange> lmtr = new ArrayList<>();
120      LinkedHashSet<MediaType> lmt = new LinkedHashSet<>();
121      List<Serializer> l = new ArrayList<>();
122      for (Serializer s : serializers) {
123         for (MediaTypeRange m: s.getMediaTypeRanges()) {
124            lmtr.add(m);
125            l.add(s);
126         }
127         for (MediaType mt : s.getAcceptMediaTypes())
128            lmt.add(mt);
129      }
130
131      this.mediaTypeRanges = lmtr.toArray(new MediaTypeRange[lmt.size()]);
132      this.mediaTypesList = unmodifiableList(new ArrayList<>(lmt));
133      this.mediaTypeRangeSerializers = l.toArray(new Serializer[l.size()]);
134   }
135
136   /**
137    * Searches the group for a serializer that can handle the specified <code>Accept</code> value.
138    *
139    * <p>
140    * The <code>accept</code> value complies with the syntax described in RFC2616, Section 14.1, as described below:
141    * <p class='bcode w800'>
142    *    Accept         = "Accept" ":"
143    *                      #( media-range [ accept-params ] )
144    *
145    *    media-range    = ( "*\/*"
146    *                      | ( type "/" "*" )
147    *                      | ( type "/" subtype )
148    *                      ) *( ";" parameter )
149    *    accept-params  = ";" "q" "=" qvalue *( accept-extension )
150    *    accept-extension = ";" token [ "=" ( token | quoted-string ) ]
151    * </p>
152    *
153    * <p>
154    * The returned object includes both the serializer and media type that matched.
155    *
156    * @param acceptHeader The HTTP <l>Accept</l> header string.
157    * @return The serializer and media type that matched the accept header, or <jk>null</jk> if no match was made.
158    */
159   public SerializerMatch getSerializerMatch(String acceptHeader) {
160      SerializerMatch sm = cache.get(acceptHeader);
161      if (sm != null)
162         return sm;
163
164      Accept a = Accept.forString(acceptHeader);
165      int match = a.findMatch(mediaTypeRanges);
166      if (match >= 0) {
167         sm = new SerializerMatch(mediaTypeRanges[match].getMediaType(), mediaTypeRangeSerializers[match]);
168         cache.putIfAbsent(acceptHeader, sm);
169      }
170
171      return cache.get(acceptHeader);
172   }
173
174   /**
175    * Same as {@link #getSerializerMatch(String)} but matches using a {@link MediaType} instance.
176    *
177    * @param mediaType The HTTP media type.
178    * @return The serializer and media type that matched the media type, or <jk>null</jk> if no match was made.
179    */
180   public SerializerMatch getSerializerMatch(MediaType mediaType) {
181      return getSerializerMatch(mediaType.toString());
182   }
183
184   /**
185    * Same as {@link #getSerializerMatch(String)} but returns just the matched serializer.
186    *
187    * @param acceptHeader The HTTP <l>Accept</l> header string.
188    * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made.
189    */
190   public Serializer getSerializer(String acceptHeader) {
191      SerializerMatch sm = getSerializerMatch(acceptHeader);
192      return sm == null ? null : sm.getSerializer();
193   }
194
195   /**
196    * Same as {@link #getSerializerMatch(MediaType)} but returns just the matched serializer.
197    *
198    * @param mediaType The HTTP media type.
199    * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made.
200    */
201   public Serializer getSerializer(MediaType mediaType) {
202      if (mediaType == null)
203         return null;
204      return getSerializer(mediaType.toString());
205   }
206
207   /**
208    * Same as {@link #getSerializer(String)}, but casts it to a {@link WriterSerializer}.
209    *
210    * @param acceptHeader The HTTP <l>Accept</l> header string.
211    * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made.
212    */
213   public WriterSerializer getWriterSerializer(String acceptHeader) {
214      return (WriterSerializer)getSerializer(acceptHeader);
215   }
216
217   /**
218    * Same as {@link #getSerializer(MediaType)}, but casts it to a {@link WriterSerializer}.
219    *
220    * @param mediaType The HTTP media type.
221    * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made.
222    */
223   public WriterSerializer getWriterSerializer(MediaType mediaType) {
224      return (WriterSerializer)getSerializer(mediaType);
225   }
226
227   /**
228    * Same as {@link #getSerializer(String)}, but casts it to an {@link OutputStreamSerializer}.
229    *
230    * @param acceptHeader The HTTP <l>Accept</l> header string.
231    * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made.
232    */
233   public OutputStreamSerializer getStreamSerializer(String acceptHeader) {
234      return (OutputStreamSerializer)getSerializer(acceptHeader);
235   }
236
237   /**
238    * Same as {@link #getSerializer(MediaType)}, but casts it to a {@link OutputStreamSerializer}.
239    *
240    * @param mediaType The HTTP media type.
241    * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made.
242    */
243   public OutputStreamSerializer getStreamSerializer(MediaType mediaType) {
244      return (OutputStreamSerializer)getSerializer(mediaType);
245   }
246
247   /**
248    * Returns the media types that all serializers in this group can handle.
249    *
250    * <p>
251    * Entries are ordered in the same order as the serializers in the group.
252    *
253    * @return An unmodifiable list of media types.
254    */
255   public List<MediaType> getSupportedMediaTypes() {
256      return mediaTypesList;
257   }
258
259   /**
260    * Returns a copy of the serializers in this group.
261    *
262    * @return An unmodifiable list of serializers in this group.
263    */
264   public List<Serializer> getSerializers() {
265      return serializers;
266   }
267}