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