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