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.http;
014
015import static org.apache.juneau.internal.CollectionUtils.*;
016import static org.apache.juneau.internal.StringUtils.*;
017
018import java.util.*;
019import java.util.concurrent.*;
020
021import org.apache.juneau.annotation.*;
022import org.apache.juneau.internal.*;
023import org.apache.juneau.json.*;
024
025
026/**
027 * Describes a single media type used in content negotiation between an HTTP client and server, as described in
028 * Section 14.1 and 14.7 of RFC2616 (the HTTP/1.1 specification).
029 *
030 * <h5 class='section'>See Also:</h5>
031 * <ul class='doctree'>
032 *    <li class='extlink'>{@doc RFC2616}
033 * </ul>
034 */
035@BeanIgnore
036public class MediaType implements Comparable<MediaType> {
037
038   private static final boolean NOCACHE = Boolean.getBoolean("juneau.nocache");
039   private static final ConcurrentHashMap<String,MediaType> CACHE = new ConcurrentHashMap<>();
040
041   /** Reusable predefined media type */
042   @SuppressWarnings("javadoc")
043   public static final MediaType
044      CSV = forString("text/csv"),
045      HTML = forString("text/html"),
046      JSON = forString("application/json"),
047      MSGPACK = forString("octal/msgpack"),
048      PLAIN = forString("text/plain"),
049      UON = forString("text/uon"),
050      URLENCODING = forString("application/x-www-form-urlencoded"),
051      XML = forString("text/xml"),
052      XMLSOAP = forString("text/xml+soap"),
053
054      RDF = forString("text/xml+rdf"),
055      RDFABBREV = forString("text/xml+rdf+abbrev"),
056      NTRIPLE = forString("text/n-triple"),
057      TURTLE = forString("text/turtle"),
058      N3 = forString("text/n3")
059   ;
060
061   private final String mediaType;
062   private final String type;                           // The media type (e.g. "text" for Accept, "utf-8" for Accept-Charset)
063   private final String subType;                        // The media sub-type (e.g. "json" for Accept, not used for Accept-Charset)
064   private final String[] subTypes;                     // The media sub-type (e.g. "json" for Accept, not used for Accept-Charset)
065   private final String[] subTypesSorted;               // Same as subTypes, but sorted so that it can be used for comparison.
066   private final List<String> subTypesList;             // The media sub-type (e.g. "json" for Accept, not used for Accept-Charset)
067   private final Map<String,Set<String>> parameters;    // The media type parameters (e.g. "text/html;level=1").  Does not include q!
068   private final boolean hasSubtypeMeta;                // The media subtype contains meta-character '*'.
069
070   /**
071    * Returns the media type for the specified string.
072    * The same media type strings always return the same objects so that these objects
073    * can be compared for equality using '=='.
074    *
075    * <h5 class='section'>Notes:</h5>
076    * <ul class='spaced-list'>
077    *    <li>
078    *       Spaces are replaced with <js>'+'</js> characters.
079    *       This gets around the issue where passing media type strings with <js>'+'</js> as HTTP GET parameters
080    *       get replaced with spaces by your browser.  Since spaces aren't supported by the spec, this
081    *       is doesn't break anything.
082    *    <li>
083    *       Anything including and following the <js>';'</js> character is ignored (e.g. <js>";charset=X"</js>).
084    * </ul>
085    *
086    * @param s
087    *    The media type string.
088    *    Will be lowercased.
089    *    Returns <jk>null</jk> if input is null or empty.
090    * @return A cached media type object.
091    */
092   public static MediaType forString(String s) {
093      if (isEmpty(s))
094         return null;
095      MediaType mt = CACHE.get(s);
096      if (mt != null)
097         return mt;
098      mt = new MediaType(s);
099      if (NOCACHE)
100         return mt;
101      CACHE.putIfAbsent(s, mt);
102      return CACHE.get(s);
103   }
104
105   /**
106    * Same as {@link #forString(String)} but allows you to construct an array of <code>MediaTypes</code> from an
107    * array of strings.
108    *
109    * @param s
110    *    The media type strings.
111    * @return
112    *    An array of <code>MediaType</code> objects.
113    *    <br>Always the same length as the input string array.
114    */
115   public static MediaType[] forStrings(String...s) {
116      MediaType[] mt = new MediaType[s.length];
117      for (int i = 0; i < s.length; i++)
118         mt[i] = forString(s[i]);
119      return mt;
120   }
121
122   MediaType(String mt) {
123      Builder b = new Builder(mt);
124      this.mediaType = b.mediaType;
125      this.type = b.type;
126      this.subType = b.subType;
127      this.subTypes = b.subTypes;
128      this.subTypesSorted = b.subTypesSorted;
129      this.subTypesList = immutableList(subTypes);
130      this.parameters = unmodifiableMap(b.parameters);
131      this.hasSubtypeMeta = b.hasSubtypeMeta;
132   }
133
134   static final class Builder {
135      String mediaType, type, subType;
136      String[] subTypes, subTypesSorted;
137      Map<String,Set<String>> parameters;
138      boolean hasSubtypeMeta;
139
140      Builder(String mt) {
141         mt = mt.trim();
142         int i = mt.indexOf(',');
143         if (i != -1)
144            mt = mt.substring(0, i);
145
146         i = mt.indexOf(';');
147         if (i == -1) {
148            this.parameters = Collections.EMPTY_MAP;
149         } else {
150            this.parameters = new TreeMap<>();
151            String[] tokens = mt.substring(i+1).split(";");
152
153            for (int j = 0; j < tokens.length; j++) {
154               String[] parm = tokens[j].split("=");
155               if (parm.length == 2) {
156                  String k = parm[0].trim(), v = parm[1].trim();
157                  if (! parameters.containsKey(k))
158                     parameters.put(k, new TreeSet<String>());
159                  parameters.get(k).add(v);
160               }
161            }
162
163            mt = mt.substring(0, i);
164         }
165
166         this.mediaType = mt;
167         if (mt != null) {
168            mt = mt.replace(' ', '+');
169            i = mt.indexOf('/');
170            type = (i == -1 ? mt : mt.substring(0, i));
171            subType = (i == -1 ? "*" : mt.substring(i+1));
172         }
173         this.subTypes = StringUtils.split(subType, '+');
174         this.subTypesSorted = Arrays.copyOf(subTypes, subTypes.length);
175         Arrays.sort(this.subTypesSorted);
176         hasSubtypeMeta = ArrayUtils.contains("*", this.subTypes);
177      }
178   }
179
180   /**
181    * Returns the <js>'type'</js> fragment of the <js>'type/subType'</js> string.
182    *
183    * @return The media type.
184    */
185   public final String getType() {
186      return type;
187   }
188
189   /**
190    * Returns the <js>'subType'</js> fragment of the <js>'type/subType'</js> string.
191    *
192    * @return The media subtype.
193    */
194   public final String getSubType() {
195      return subType;
196   }
197
198   /**
199    * Returns <jk>true</jk> if the subtype contains the specified <js>'+'</js> delimited subtype value.
200    *
201    * @param st
202    *    The subtype string.
203    *    Case is ignored.
204    * @return <jk>true</jk> if the subtype contains the specified subtype string.
205    */
206   public final boolean hasSubType(String st) {
207      if (st != null)
208         for (String s : subTypes)
209            if (st.equalsIgnoreCase(s))
210               return true;
211      return false;
212   }
213
214   /**
215    * Returns the subtypes broken down by fragments delimited by <js>"'"</js>.
216    *
217    * <P>
218    * For example, the media type <js>"text/foo+bar"</js> will return a list of
219    * <code>[<js>'foo'</js>,<js>'bar'</js>]</code>
220    *
221    * @return An unmodifiable list of subtype fragments.  Never <jk>null</jk>.
222    */
223   public final List<String> getSubTypes() {
224      return subTypesList;
225   }
226
227   /**
228    * Returns <jk>true</jk> if this media type contains the <js>'*'</js> meta character.
229    *
230    * @return <jk>true</jk> if this media type contains the <js>'*'</js> meta character.
231    */
232   public final boolean isMeta() {
233      return hasSubtypeMeta;
234   }
235
236   /**
237    * Returns a match metric against the specified media type where a larger number represents a better match.
238    *
239    * <p>
240    * This media type can contain <js>'*'</js> metacharacters.
241    * <br>The comparison media type must not.
242    *
243    * <ul>
244    *    <li>Exact matches (e.g. <js>"text/json"<js>/</js>"text/json"</js>) should match
245    *       better than meta-character matches (e.g. <js>"text/*"<js>/</js>"text/json"</js>)
246    *    <li>The comparison media type can have additional subtype tokens (e.g. <js>"text/json+foo"</js>)
247    *       that will not prevent a match if the <code>allowExtraSubTypes</code> flag is set.
248    *       The reverse is not true, e.g. the comparison media type must contain all subtype tokens found in the
249    *       comparing media type.
250    *       <ul>
251    *          <li>We want the {@link JsonSerializer} (<js>"text/json"</js>) class to be able to handle requests for <js>"text/json+foo"</js>.
252    *          <li>We want to make sure {@link org.apache.juneau.json.SimpleJsonSerializer} (<js>"text/json+simple"</js>) does not handle
253    *             requests for <js>"text/json"</js>.
254    *       </ul>
255    *       More token matches should result in a higher match number.
256    * </ul>
257    *
258    * The formula is as follows for <code>type/subTypes</code>:
259    * <ul>
260    *    <li>An exact match is <code>100,000</code>.
261    *    <li>Add the following for type (assuming subtype match is &lt;0):
262    *    <ul>
263    *       <li><code>10,000</code> for an exact match (e.g. <js>"text"</js>==<js>"text"</js>).
264    *       <li><code>5,000</code> for a meta match (e.g. <js>"*"</js>==<js>"text"</js>).
265    *    </ul>
266    *    <li>Add the following for subtype (assuming type match is &lt;0):
267    *    <ul>
268    *       <li><code>7,500</code> for an exact match (e.g. <js>"json+foo"</js>==<js>"json+foo"</js> or <js>"json+foo"</js>==<js>"foo+json"</js>)
269    *       <li><code>100</code> for every subtype entry match (e.g. <js>"json"</js>/<js>"json+foo"</js>)
270    *    </ul>
271    * </ul>
272    *
273    * @param o The media type to compare with.
274    * @param allowExtraSubTypes If <jk>true</jk>,
275    * @return <jk>true</jk> if the media types match.
276    */
277   public final int match(MediaType o, boolean allowExtraSubTypes) {
278
279      // Perfect match
280      if (this == o || (type.equals(o.type) && subType.equals(o.subType)))
281         return 100000;
282
283      int c = 0;
284
285      if (type.equals(o.type))
286         c += 10000;
287      else if ("*".equals(type) || "*".equals(o.type))
288         c += 5000;
289
290      if (c == 0)
291         return 0;
292
293      // Subtypes match but are ordered different
294      if (ArrayUtils.equals(subTypesSorted, o.subTypesSorted))
295         return c + 7500;
296
297      for (String st1 : subTypes) {
298         if ("*".equals(st1))
299            c += 0;
300         else if (ArrayUtils.contains(st1, o.subTypes))
301            c += 100;
302         else if (o.hasSubtypeMeta)
303            c += 0;
304         else
305            return 0;
306      }
307      for (String st2 : o.subTypes) {
308         if ("*".equals(st2))
309            c += 0;
310         else if (ArrayUtils.contains(st2, subTypes))
311            c += 100;
312         else if (hasSubtypeMeta)
313            c += 0;
314         else if (! allowExtraSubTypes)
315            return 0;
316         else
317            c += 10;
318      }
319
320      return c;
321   }
322
323   /**
324    * Returns the additional parameters on this media type.
325    *
326    * <p>
327    * For example, given the media type string <js>"text/html;level=1"</js>, will return a map
328    * with the single entry <code>{level:[<js>'1'</js>]}</code>.
329    *
330    * @return The map of additional parameters, or an empty map if there are no parameters.
331    */
332   public final Map<String,Set<String>> getParameters() {
333      return parameters;
334   }
335
336   @Override /* Object */
337   public final String toString() {
338      if (parameters.isEmpty())
339         return mediaType;
340      StringBuilder sb = new StringBuilder(mediaType);
341      for (Map.Entry<String,Set<String>> e : parameters.entrySet())
342         for (String value : e.getValue())
343            sb.append(';').append(e.getKey()).append('=').append(value);
344      return sb.toString();
345   }
346
347   @Override /* Object */
348   public final int hashCode() {
349      return mediaType.hashCode();
350   }
351
352   @Override /* Object */
353   public final boolean equals(Object o) {
354      return this == o;
355   }
356
357   @Override
358   public final int compareTo(MediaType o) {
359      return mediaType.compareTo(o.mediaType);
360   }
361}