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'>
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 BeanContext {
067
068   // Maps Accept headers to matching serializers.
069   private final ConcurrentHashMap<String,SerializerMatch> cache = new ConcurrentHashMap<>();
070
071   private final MediaType[] mediaTypes;
072   private final List<MediaType> mediaTypesList;
073   private final Serializer[] mediaTypeSerializers;
074   private final List<Serializer> serializers;
075
076   /**
077    * Instantiates a new clean-slate {@link SerializerGroupBuilder} object.
078    * 
079    * <p>
080    * This is equivalent to simply calling <code><jk>new</jk> SerializerGroupBuilder()</code>.
081    * 
082    * @return A new {@link SerializerGroupBuilder} object.
083    */
084   public static SerializerGroupBuilder create() {
085      return new SerializerGroupBuilder();
086   }
087   
088   /**
089    * Returns a builder that's a copy of the settings on this serializer group.
090    * 
091    * @return A new {@link SerializerGroupBuilder} initialized to this group.
092    */
093   @Override /* Context */
094   public SerializerGroupBuilder builder() {
095      return new SerializerGroupBuilder(this);
096   }
097
098   /**
099    * Constructor.
100    * 
101    * @param ps
102    *    The modifiable properties that were used to initialize the serializers.
103    *    A snapshot of these will be made so that we can clone and modify this group.
104    * @param serializers
105    *    The serializers defined in this group.
106    *    The order is important because they will be tried in reverse order (e.g.newer first) in which they will be tried
107    *    to match against media types.
108    */
109   public SerializerGroup(PropertyStore ps, Serializer[] serializers) {
110      super(ps);
111      this.serializers = immutableList(serializers);
112
113      List<MediaType> lmt = new ArrayList<>();
114      List<Serializer> l = new ArrayList<>();
115      for (Serializer s : serializers) {
116         for (MediaType m: s.getMediaTypes()) {
117            lmt.add(m);
118            l.add(s);
119         }
120      }
121
122      this.mediaTypes = lmt.toArray(new MediaType[lmt.size()]);
123      this.mediaTypesList = unmodifiableList(lmt);
124      this.mediaTypeSerializers = l.toArray(new Serializer[l.size()]);
125   }
126
127   /**
128    * Searches the group for a serializer that can handle the specified <code>Accept</code> value.
129    * 
130    * <p>
131    * The <code>accept</code> value complies with the syntax described in RFC2616, Section 14.1, as described below:
132    * <p class='bcode'>
133    *    Accept         = "Accept" ":"
134    *                      #( media-range [ accept-params ] )
135    * 
136    *    media-range    = ( "*\/*"
137    *                      | ( type "/" "*" )
138    *                      | ( type "/" subtype )
139    *                      ) *( ";" parameter )
140    *    accept-params  = ";" "q" "=" qvalue *( accept-extension )
141    *    accept-extension = ";" token [ "=" ( token | quoted-string ) ]
142    * </p>
143    * 
144    * <p>
145    * The returned object includes both the serializer and media type that matched.
146    * 
147    * @param acceptHeader The HTTP <l>Accept</l> header string.
148    * @return The serializer and media type that matched the accept header, or <jk>null</jk> if no match was made.
149    */
150   public SerializerMatch getSerializerMatch(String acceptHeader) {
151      SerializerMatch sm = cache.get(acceptHeader);
152      if (sm != null)
153         return sm;
154
155      Accept a = Accept.forString(acceptHeader);
156      int match = a.findMatch(mediaTypes);
157      if (match >= 0) {
158         sm = new SerializerMatch(mediaTypes[match], mediaTypeSerializers[match]);
159         cache.putIfAbsent(acceptHeader, sm);
160      }
161
162      return cache.get(acceptHeader);
163   }
164
165   /**
166    * Same as {@link #getSerializerMatch(String)} but matches using a {@link MediaType} instance.
167    * 
168    * @param mediaType The HTTP media type.
169    * @return The serializer and media type that matched the media type, or <jk>null</jk> if no match was made.
170    */
171   public SerializerMatch getSerializerMatch(MediaType mediaType) {
172      return getSerializerMatch(mediaType.toString());
173   }
174
175   /**
176    * Same as {@link #getSerializerMatch(String)} but returns just the matched serializer.
177    * 
178    * @param acceptHeader The HTTP <l>Accept</l> header string.
179    * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made.
180    */
181   public Serializer getSerializer(String acceptHeader) {
182      SerializerMatch sm = getSerializerMatch(acceptHeader);
183      return sm == null ? null : sm.getSerializer();
184   }
185
186   /**
187    * Same as {@link #getSerializerMatch(MediaType)} but returns just the matched serializer.
188    * 
189    * @param mediaType The HTTP media type.
190    * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made.
191    */
192   public Serializer getSerializer(MediaType mediaType) {
193      if (mediaType == null)
194         return null;
195      return getSerializer(mediaType.toString());
196   }
197
198   /**
199    * Same as {@link #getSerializer(String)}, but casts it to a {@link WriterSerializer}.
200    * 
201    * @param acceptHeader The HTTP <l>Accept</l> header string.
202    * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made.
203    */
204   public WriterSerializer getWriterSerializer(String acceptHeader) {
205      return (WriterSerializer)getSerializer(acceptHeader);
206   }
207
208   /**
209    * Same as {@link #getSerializer(MediaType)}, but casts it to a {@link WriterSerializer}.
210    * 
211    * @param mediaType The HTTP media type.
212    * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made.
213    */
214   public WriterSerializer getWriterSerializer(MediaType mediaType) {
215      return (WriterSerializer)getSerializer(mediaType);
216   }
217
218   /**
219    * Same as {@link #getSerializer(String)}, but casts it to an {@link OutputStreamSerializer}.
220    * 
221    * @param acceptHeader The HTTP <l>Accept</l> header string.
222    * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made.
223    */
224   public OutputStreamSerializer getStreamSerializer(String acceptHeader) {
225      return (OutputStreamSerializer)getSerializer(acceptHeader);
226   }
227
228   /**
229    * Same as {@link #getSerializer(MediaType)}, but casts it to a {@link OutputStreamSerializer}.
230    * 
231    * @param mediaType The HTTP media type.
232    * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made.
233    */
234   public OutputStreamSerializer getStreamSerializer(MediaType mediaType) {
235      return (OutputStreamSerializer)getSerializer(mediaType);
236   }
237
238   /**
239    * Returns the media types that all serializers in this group can handle.
240    * 
241    * <p>
242    * Entries are ordered in the same order as the serializers in the group.
243    * 
244    * @return An unmodifiable list of media types.
245    */
246   public List<MediaType> getSupportedMediaTypes() {
247      return mediaTypesList;
248   }
249
250   /**
251    * Returns a copy of the serializers in this group.
252    * 
253    * @return An unmodifiable list of serializers in this group.
254    */
255   public List<Serializer> getSerializers() {
256      return serializers;
257   }
258}