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