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;
014
015import static org.apache.juneau.common.internal.StringUtils.*;
016import static org.apache.juneau.internal.CollectionUtils.*;
017import static org.apache.juneau.internal.ObjectUtils.*;
018
019import java.util.*;
020import java.util.function.*;
021
022import org.apache.http.*;
023import org.apache.http.message.*;
024import org.apache.juneau.annotation.*;
025import org.apache.juneau.common.internal.*;
026import org.apache.juneau.internal.*;
027import org.apache.juneau.json.*;
028
029
030/**
031 * Describes a single media type used in content negotiation between an HTTP client and server, as described in
032 * Section 14.1 and 14.7 of RFC2616 (the HTTP/1.1 specification).
033 *
034 * <h5 class='section'>See Also:</h5><ul>
035 *    <li class='link'><a class="doclink" href="../../../index.html#juneau-rest-common">juneau-rest-common</a>
036 *    <li class='extlink'><a class="doclink" href="https://www.w3.org/Protocols/rfc2616/rfc2616.html">Hypertext Transfer Protocol -- HTTP/1.1</a>
037 * </ul>
038 */
039@BeanIgnore
040public class MediaType implements Comparable<MediaType>  {
041
042   //-----------------------------------------------------------------------------------------------------------------
043   // Static
044   //-----------------------------------------------------------------------------------------------------------------
045
046   /** Represents an empty media type object. */
047   public static final MediaType EMPTY = new MediaType("/*");
048
049   private static final Cache<String,MediaType> CACHE = Cache.of(String.class, MediaType.class).build();
050
051   /** Reusable predefined media type */
052   @SuppressWarnings("javadoc")
053   public static final MediaType
054      CSV = of("text/csv"),
055      HTML = of("text/html"),
056      JSON = of("application/json"),
057      MSGPACK = of("octal/msgpack"),
058      PLAIN = of("text/plain"),
059      UON = of("text/uon"),
060      URLENCODING = of("application/x-www-form-urlencoded"),
061      XML = of("text/xml"),
062      XMLSOAP = of("text/xml+soap"),
063
064      RDF = of("text/xml+rdf"),
065      RDFABBREV = of("text/xml+rdf+abbrev"),
066      NTRIPLE = of("text/n-triple"),
067      TURTLE = of("text/turtle"),
068      N3 = of("text/n3")
069   ;
070
071   /**
072    * Returns the media type for the specified string.
073    * The same media type strings always return the same objects so that these objects
074    * can be compared for equality using '=='.
075    *
076    * <h5 class='section'>Notes:</h5><ul>
077    *    <li class='note'>
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 class='note'>
083    *       Anything including and following the <js>';'</js> character is ignored (e.g. <js>";charset=X"</js>).
084    * </ul>
085    *
086    * @param value
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 of(String value) {
093      return value == null ? null : CACHE.get(value, ()->new MediaType(value));
094   }
095
096   /**
097    * Same as {@link #of(String)} but allows you to specify the parameters.
098    *
099    *
100    * @param value
101    *    The media type string.
102    *    Will be lowercased.
103    *    Returns <jk>null</jk> if input is null or empty.
104    * @param parameters The media type parameters.  If <jk>null</jk>, they're pulled from the media type string.
105    * @return A new media type object, cached if parameters were not specified.
106    */
107   public static MediaType of(String value, NameValuePair...parameters) {
108      if (parameters.length == 0)
109         return of(value);
110      return isEmpty(value) ? null : new MediaType(value, parameters);
111   }
112
113   /**
114    * Same as {@link #of(String)} but allows you to construct an array of <c>MediaTypes</c> from an
115    * array of strings.
116    *
117    * @param values
118    *    The media type strings.
119    * @return
120    *    An array of <c>MediaType</c> objects.
121    *    <br>Always the same length as the input string array.
122    */
123   public static MediaType[] ofAll(String...values) {
124      MediaType[] mt = new MediaType[values.length];
125      for (int i = 0; i < values.length; i++)
126         mt[i] = of(values[i]);
127      return mt;
128   }
129
130   //-----------------------------------------------------------------------------------------------------------------
131   // Instance
132   //-----------------------------------------------------------------------------------------------------------------
133
134   private final String string;                          // The entire unparsed value.
135   private final String mediaType;                      // The "type/subtype" portion of the media type..
136   private final String type;                           // The media type (e.g. "text" for Accept, "utf-8" for Accept-Charset)
137   private final String subType;                        // The media sub-type (e.g. "json" for Accept, not used for Accept-Charset)
138   private final String[] subTypes;                     // The media sub-type (e.g. "json" for Accept, not used for Accept-Charset)
139   private final String[] subTypesSorted;               // Same as subTypes, but sorted so that it can be used for comparison.
140   private final boolean hasSubtypeMeta;                // The media subtype contains meta-character '*'.
141
142   private final NameValuePair[] parameters;            // The media type parameters (e.g. "text/html;level=1").  Does not include q!
143
144   /**
145    * Constructor.
146    *
147    * @param mt The media type string.
148    */
149   public MediaType(String mt) {
150      this(parse(mt));
151   }
152
153   /**
154    * Constructor.
155    *
156    * @param mt The media type string.
157    * @param parameters The media type parameters.  If <jk>null</jk>, they're pulled from the media type string.
158    */
159   public MediaType(String mt, NameValuePair[] parameters) {
160      this(parse(mt), parameters);
161   }
162
163   /**
164    * Constructor.
165    *
166    * @param e The parsed media type string.
167    */
168   public MediaType(HeaderElement e) {
169      this(e, null);
170   }
171
172   /**
173    * Constructor.
174    *
175    * @param e The parsed media type string.
176    * @param parameters Optional parameters.
177    */
178   public MediaType(HeaderElement e, NameValuePair[] parameters) {
179      mediaType = e.getName();
180
181      if (parameters == null) {
182         parameters = e.getParameters();
183         for (int i = 0; i < parameters.length; i++) {
184            if (parameters[i].getName().equals("q")) {
185               parameters = Arrays.copyOfRange(parameters, 0, i);
186               break;
187            }
188         }
189      }
190      for (int i = 0; i < parameters.length; i++)
191         parameters[i] = new BasicNameValuePair(parameters[i].getName(), parameters[i].getValue());
192      this.parameters = parameters;
193
194      String x = mediaType.replace(' ', '+');
195      int i = x.indexOf('/');
196      type = (i == -1 ? x : x.substring(0, i));
197      subType = (i == -1 ? "*" : x.substring(i+1));
198
199      subTypes = StringUtils.split(subType, '+');
200      subTypesSorted = Arrays.copyOf(subTypes, subTypes.length);
201      Arrays.sort(this.subTypesSorted);
202      hasSubtypeMeta = ArrayUtils.contains("*", this.subTypes);
203
204      StringBuilder sb = new StringBuilder();
205      sb.append(mediaType);
206      for (NameValuePair p : parameters)
207         sb.append(';').append(p.getName()).append('=').append(p.getValue());
208      this.string = sb.toString();
209   }
210
211   /**
212    * Returns the <js>'type'</js> fragment of the <js>'type/subType'</js> string.
213    *
214    * @return The media type.
215    */
216   public final String getType() {
217      return type;
218   }
219
220   /**
221    * Returns the <js>'subType'</js> fragment of the <js>'type/subType'</js> string.
222    *
223    * @return The media subtype.
224    */
225   public final String getSubType() {
226      return subType;
227   }
228
229   /**
230    * Returns <jk>true</jk> if the subtype contains the specified <js>'+'</js> delimited subtype value.
231    *
232    * @param st
233    *    The subtype string.
234    *    Case is ignored.
235    * @return <jk>true</jk> if the subtype contains the specified subtype string.
236    */
237   public final boolean hasSubType(String st) {
238      if (st != null)
239         for (String s : subTypes)
240            if (st.equalsIgnoreCase(s))
241               return true;
242      return false;
243   }
244
245   /**
246    * Returns the subtypes broken down by fragments delimited by <js>"'"</js>.
247    *
248    * <P>
249    * For example, the media type <js>"text/foo+bar"</js> will return a list of
250    * <code>[<js>'foo'</js>,<js>'bar'</js>]</code>
251    *
252    * @return An unmodifiable list of subtype fragments.  Never <jk>null</jk>.
253    */
254   public final List<String> getSubTypes() {
255      return ulist(subTypes);
256   }
257
258   /**
259    * Performs an action on the subtypes broken down by fragments delimited by <js>"'"</js>.
260    *
261    * @param action The action to perform.
262    * @return This object.
263    */
264   public final MediaType forEachSubType(Consumer<String> action) {
265      for (String s : subTypes)
266         action.accept(s);
267      return this;
268   }
269
270   /**
271    * Returns <jk>true</jk> if this media type subtype contains the <js>'*'</js> meta character.
272    *
273    * @return <jk>true</jk> if this media type subtype contains the <js>'*'</js> meta character.
274    */
275   public final boolean isMetaSubtype() {
276      return hasSubtypeMeta;
277   }
278
279   /**
280    * Returns a match metric against the specified media type where a larger number represents a better match.
281    *
282    * <p>
283    * This media type can contain <js>'*'</js> metacharacters.
284    * <br>The comparison media type must not.
285    *
286    * <ul>
287    *    <li>Exact matches (e.g. <js>"text/json"</js>/</js>"text/json"</js>) should match
288    *       better than meta-character matches (e.g. <js>"text/*"</js>/</js>"text/json"</js>)
289    *    <li>The comparison media type can have additional subtype tokens (e.g. <js>"text/json+foo"</js>)
290    *       that will not prevent a match if the <c>allowExtraSubTypes</c> flag is set.
291    *       The reverse is not true, e.g. the comparison media type must contain all subtype tokens found in the
292    *       comparing media type.
293    *       <ul>
294    *          <li>We want the {@link JsonSerializer} (<js>"text/json"</js>) class to be able to handle requests for <js>"text/json+foo"</js>.
295    *          <li>We want to make sure {@link org.apache.juneau.json.Json5Serializer} (<js>"text/json5"</js>) does not handle
296    *             requests for <js>"text/json"</js>.
297    *       </ul>
298    *       More token matches should result in a higher match number.
299    * </ul>
300    *
301    * The formula is as follows for <c>type/subTypes</c>:
302    * <ul>
303    *    <li>An exact match is <c>100,000</c>.
304    *    <li>Add the following for type (assuming subtype match is &lt;0):
305    *    <ul>
306    *       <li><c>10,000</c> for an exact match (e.g. <js>"text"</js>==<js>"text"</js>).
307    *       <li><c>5,000</c> for a meta match (e.g. <js>"*"</js>==<js>"text"</js>).
308    *    </ul>
309    *    <li>Add the following for subtype (assuming type match is &lt;0):
310    *    <ul>
311    *       <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>)
312    *       <li><c>100</c> for every subtype entry match (e.g. <js>"json"</js>/<js>"json+foo"</js>)
313    *    </ul>
314    * </ul>
315    *
316    * @param o The media type to compare with.
317    * @param allowExtraSubTypes If <jk>true</jk>,
318    * @return <jk>true</jk> if the media types match.
319    */
320   public final int match(MediaType o, boolean allowExtraSubTypes) {
321
322      if (o == null)
323         return -1;
324
325      // Perfect match
326      if (this == o || (type.equals(o.type) && subType.equals(o.subType)))
327         return 100000;
328
329      int c = 0;
330
331      if (type.equals(o.type))
332         c += 10000;
333      else if ("*".equals(type) || "*".equals(o.type))
334         c += 5000;
335
336      if (c == 0)
337         return 0;
338
339      // Subtypes match but are ordered different
340      if (ArrayUtils.equals(subTypesSorted, o.subTypesSorted))
341         return c + 7500;
342
343      for (String st1 : subTypes) {
344         if ("*".equals(st1))
345            c += 0;
346         else if (ArrayUtils.contains(st1, o.subTypes))
347            c += 100;
348         else if (o.hasSubtypeMeta)
349            c += 0;
350         else
351            return 0;
352      }
353      for (String st2 : o.subTypes) {
354         if ("*".equals(st2))
355            c += 0;
356         else if (ArrayUtils.contains(st2, subTypes))
357            c += 100;
358         else if (hasSubtypeMeta)
359            c += 0;
360         else if (! allowExtraSubTypes)
361            return 0;
362         else
363            c += 10;
364      }
365
366      return c;
367   }
368
369   /**
370    * Returns the additional parameters on this media type.
371    *
372    * <p>
373    * For example, given the media type string <js>"text/html;level=1"</js>, will return a map
374    * with the single entry <code>{level:[<js>'1'</js>]}</code>.
375    *
376    * @return The map of additional parameters, or an empty map if there are no parameters.
377    */
378   public List<NameValuePair> getParameters() {
379      return ulist(parameters);
380   }
381
382   /**
383    * Performs an action on the additional parameters on this media type.
384    *
385    * @param action The action to perform.
386    * @return This object.
387    */
388   public MediaType forEachParameter(Consumer<NameValuePair> action) {
389      for (NameValuePair p : parameters)
390         action.accept(p);
391      return this;
392   }
393
394   /**
395    * Returns the additional parameter on this media type.
396    *
397    * @param name The additional parameter name.
398    * @return The parameter value, or <jk>null</jk> if not found.
399    */
400   public String getParameter(String name) {
401      for (NameValuePair p : parameters)
402         if (eq(name, p.getName()))
403            return p.getValue();
404      return null;
405   }
406
407   /**
408    * Given a list of media types, returns the best match for this <c>Content-Type</c> header.
409    *
410    * <p>
411    * Note that fuzzy matching is allowed on the media types where the <c>Content-Types</c> header may
412    * contain additional subtype parts.
413    * <br>For example, given a <c>Content-Type</c> value of <js>"text/json+activity"</js>,
414    * the media type <js>"text/json"</js> will match if <js>"text/json+activity"</js> or <js>"text/activity+json"</js>
415    * isn't found.
416    * <br>The purpose for this is to allow parsers to match when artifacts such as <c>id</c> properties are
417    * present in the header.
418    *
419    * @param mediaTypes The media types to match against.
420    * @return The index into the array of the best match, or <c>-1</c> if no suitable matches could be found.
421    */
422   public int match(List<MediaType> mediaTypes) {
423      int matchQuant = 0, matchIndex = -1;
424
425      for (int i = 0; i < mediaTypes.size(); i++) {
426         MediaType mt = mediaTypes.get(i);
427         int matchQuant2 = mt.match(this, true);
428         if (matchQuant2 > matchQuant) {
429            matchQuant = matchQuant2;
430            matchIndex = i;
431         }
432      }
433      return matchIndex;
434   }
435
436   private static HeaderElement parse(String value) {
437      HeaderElement[] elements = BasicHeaderValueParser.parseElements(emptyIfNull(trim(value)), null);
438      return (elements.length > 0 ? elements[0] : new BasicHeaderElement("", ""));
439   }
440
441   @Override /* Object */
442   public String toString() {
443      return string;
444   }
445
446   @Override /* Object */
447   public int hashCode() {
448      return string.hashCode();
449   }
450
451   @Override /* Object */
452   public boolean equals(Object o) {
453      return (o instanceof MediaType) && eq(this, (MediaType)o, (x,y)->eq(x.string, y.string));
454   }
455
456   @Override
457   public final int compareTo(MediaType o) {
458      return toString().compareTo(o.toString());
459   }
460}