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