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