    001    /*
    002     * Copyright (C) 2011 The Guava Authors
    003     *
    004     * Licensed under the Apache License, Version 2.0 (the "License");
    005     * you may not use this file except in compliance with the License.
    006     * You may obtain a copy of the License at
    007     *
    008     * http://www.apache.org/licenses/LICENSE-2.0
    009     *
    010     * Unless required by applicable law or agreed to in writing, software
    011     * distributed under the License is distributed on an "AS IS" BASIS,
    012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    013     * See the License for the specific language governing permissions and
    014     * limitations under the License.
    015     */
    016
    017    package com.google.common.net;
    018
    019    import static com.google.common.base.CharMatcher.ASCII;
    020    import static com.google.common.base.CharMatcher.JAVA_ISO_CONTROL;
    021    import static com.google.common.base.Charsets.UTF_8;
    022    import static com.google.common.base.Preconditions.checkArgument;
    023    import static com.google.common.base.Preconditions.checkNotNull;
    024    import static com.google.common.base.Preconditions.checkState;
    025
    026    import com.google.common.annotations.Beta;
    027    import com.google.common.annotations.GwtCompatible;
    028    import com.google.common.base.Ascii;
    029    import com.google.common.base.CharMatcher;
    030    import com.google.common.base.Function;
    031    import com.google.common.base.Joiner;
    032    import com.google.common.base.Joiner.MapJoiner;
    033    import com.google.common.base.Objects;
    034    import com.google.common.base.Optional;
    035    import com.google.common.collect.ImmutableListMultimap;
    036    import com.google.common.collect.ImmutableMap;
    037    import com.google.common.collect.ImmutableMultiset;
    038    import com.google.common.collect.ImmutableSet;
    039    import com.google.common.collect.Iterables;
    040    import com.google.common.collect.Maps;
    041    import com.google.common.collect.Multimap;
    042    import com.google.common.collect.Multimaps;
    043
    044    import java.nio.charset.Charset;
    045    import java.nio.charset.IllegalCharsetNameException;
    046    import java.nio.charset.UnsupportedCharsetException;
    047    import java.util.Collection;
    048    import java.util.Map;
    049    import java.util.Map.Entry;
    050
    051    import javax.annotation.Nullable;
    052    import javax.annotation.concurrent.Immutable;
    053
    054    /**
    055     * Represents an <a href="http://en.wikipedia.org/wiki/Internet_media_type">Internet Media Type</a>
    056     * (also known as a MIME Type or Content Type). This class also supports the concept of media ranges
    057     * <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1">defined by HTTP/1.1</a>.
    058     * As such, the {@code *} character is treated as a wildcard and is used to represent any acceptable
    059     * type or subtype value. A media type may not have wildcard type with a declared subtype. The
    060     * {@code *} character has no special meaning as part of a parameter. All values for type, subtype,
    061     * parameter attributes or parameter values must be valid according to RFCs
    062     * <a href="http://www.ietf.org/rfc/rfc2045.txt">2045</a> and
    063     * <a href="http://www.ietf.org/rfc/rfc2046.txt">2046</a>.
    064     *
    065     * <p>All portions of the media type that are case-insensitive (type, subtype, parameter attributes)
    066     * are normalized to lowercase. The value of the {@code charset} parameter is normalized to
    067     * lowercase, but all others are left as-is.
    068     *
    069     * <p>Note that this specifically does <strong>not</strong> represent the value of the MIME
    070     * {@code Content-Type} header and as such has no support for header-specific considerations such as
    071     * line folding and comments.
    072     *
    073     * <p>For media types that take a charset the predefined constants default to UTF-8 and have a
    074     * "_UTF_8" suffix. To get a version without a character set, use {@link #withoutParameters}.
    075     *
    076     * @since 12.0
    077     *
    078     * @author Gregory Kick
    079     */
    080    @Beta
    081    @GwtCompatible
    082    @Immutable
    083    public final class MediaType {
    084      private static final String CHARSET_ATTRIBUTE = "charset";
    085      private static final ImmutableListMultimap<String, String> UTF_8_CONSTANT_PARAMETERS =
    086          ImmutableListMultimap.of(CHARSET_ATTRIBUTE, Ascii.toLowerCase(UTF_8.name()));
    087
    088      /** Matcher for type, subtype and attributes. */
    089      private static final CharMatcher TOKEN_MATCHER = ASCII.and(JAVA_ISO_CONTROL.negate())
    090          .and(CharMatcher.isNot(' '))
    091          .and(CharMatcher.noneOf("()<>@,;:\\\"/[]?="));
    092      private static final CharMatcher QUOTED_TEXT_MATCHER = ASCII
    093          .and(CharMatcher.noneOf("\"\\\r"));
    094      /*
    095       * This matches the same characters as linear-white-space from RFC 822, but we make no effort to
    096       * enforce any particular rules with regards to line folding as stated in the class docs.
    097       */
    098      private static final CharMatcher LINEAR_WHITE_SPACE = CharMatcher.anyOf(" \t\r\n");
    099
    100      // TODO(gak): make these public?
    101      private static final String APPLICATION_TYPE = "application";
    102      private static final String AUDIO_TYPE = "audio";
    103      private static final String IMAGE_TYPE = "image";
    104      private static final String TEXT_TYPE = "text";
    105      private static final String VIDEO_TYPE = "video";
    106
    107      private static final String WILDCARD = "*";
    108
    109      /*
    110       * The following constants are grouped by their type and ordered alphabetically by the constant
    111       * name within that type. The constant name should be a sensible identifier that is closest to the
    112       * "common name" of the media.  This is often, but not necessarily the same as the subtype.
    113       *
    114       * Be sure to declare all constants with the type and subtype in all lowercase.
    115       *
    116       * When adding constants, be sure to add an entry into the KNOWN_TYPES map. For types that
    117       * take a charset (e.g. all text/* types), default to UTF-8 and suffix with "_UTF_8".
    118       */
    119
    120      public static final MediaType ANY_TYPE = createConstant(WILDCARD, WILDCARD);
    121      public static final MediaType ANY_TEXT_TYPE = createConstant(TEXT_TYPE, WILDCARD);
    122      public static final MediaType ANY_IMAGE_TYPE = createConstant(IMAGE_TYPE, WILDCARD);
    123      public static final MediaType ANY_AUDIO_TYPE = createConstant(AUDIO_TYPE, WILDCARD);
    124      public static final MediaType ANY_VIDEO_TYPE = createConstant(VIDEO_TYPE, WILDCARD);
    125      public static final MediaType ANY_APPLICATION_TYPE = createConstant(APPLICATION_TYPE, WILDCARD);
    126
    127      /* text types */
    128      public static final MediaType CACHE_MANIFEST_UTF_8 =
    129          createConstantUtf8(TEXT_TYPE, "cache-manifest");
    130      public static final MediaType CSS_UTF_8 = createConstantUtf8(TEXT_TYPE, "css");
    131      public static final MediaType CSV_UTF_8 = createConstantUtf8(TEXT_TYPE, "csv");
    132      public static final MediaType HTML_UTF_8 = createConstantUtf8(TEXT_TYPE, "html");
    133      public static final MediaType I_CALENDAR_UTF_8 = createConstantUtf8(TEXT_TYPE, "calendar");
    134      public static final MediaType PLAIN_TEXT_UTF_8 = createConstantUtf8(TEXT_TYPE, "plain");
    135      /**
    136       * <a href="http://www.rfc-editor.org/rfc/rfc4329.txt">RFC 4329</a> declares
    137       * {@link #JAVASCRIPT_UTF_8 application/javascript} to be the correct media type for JavaScript,
    138       * but this may be necessary in certain situations for compatibility.
    139       */
    140      public static final MediaType TEXT_JAVASCRIPT_UTF_8 = createConstantUtf8(TEXT_TYPE, "javascript");
    141      public static final MediaType VCARD_UTF_8 = createConstantUtf8(TEXT_TYPE, "vcard");
    142      public static final MediaType WML_UTF_8 = createConstantUtf8(TEXT_TYPE, "vnd.wap.wml");
    143      public static final MediaType XML_UTF_8 = createConstantUtf8(TEXT_TYPE, "xml");
    144
    145      /* image types */
    146      public static final MediaType BMP = createConstant(IMAGE_TYPE, "bmp");
    147      public static final MediaType GIF = createConstant(IMAGE_TYPE, "gif");
    148      public static final MediaType ICO = createConstant(IMAGE_TYPE, "vnd.microsoft.icon");
    149      public static final MediaType JPEG = createConstant(IMAGE_TYPE, "jpeg");
    150      public static final MediaType PNG = createConstant(IMAGE_TYPE, "png");
    151      public static final MediaType SVG_UTF_8 = createConstantUtf8(IMAGE_TYPE, "svg+xml");
    152      public static final MediaType TIFF = createConstant(IMAGE_TYPE, "tiff");
    153      public static final MediaType WEBP = createConstant(IMAGE_TYPE, "webp");
    154
    155      /* audio types */
    156      public static final MediaType MP4_AUDIO = createConstant(AUDIO_TYPE, "mp4");
    157      public static final MediaType MPEG_AUDIO = createConstant(AUDIO_TYPE, "mpeg");
    158      public static final MediaType OGG_AUDIO = createConstant(AUDIO_TYPE, "ogg");
    159      public static final MediaType WEBM_AUDIO = createConstant(AUDIO_TYPE, "webm");
    160
    161      /* video types */
    162      public static final MediaType MP4_VIDEO = createConstant(VIDEO_TYPE, "mp4");
    163      public static final MediaType MPEG_VIDEO = createConstant(VIDEO_TYPE, "mpeg");
    164      public static final MediaType OGG_VIDEO = createConstant(VIDEO_TYPE, "ogg");
    165      public static final MediaType QUICKTIME = createConstant(VIDEO_TYPE, "quicktime");
    166      public static final MediaType WEBM_VIDEO = createConstant(VIDEO_TYPE, "webm");
    167      public static final MediaType WMV = createConstant(VIDEO_TYPE, "x-ms-wmv");
    168
    169      /* application types */
    170      public static final MediaType ATOM_UTF_8 = createConstantUtf8(APPLICATION_TYPE, "atom+xml");
    171      public static final MediaType BZIP2 = createConstant(APPLICATION_TYPE, "x-bzip2");
    172      public static final MediaType FORM_DATA = createConstant(APPLICATION_TYPE,
    173          "x-www-form-urlencoded");
    174      public static final MediaType GZIP = createConstant(APPLICATION_TYPE, "x-gzip");
    175       /**
    176        * <a href="http://www.rfc-editor.org/rfc/rfc4329.txt">RFC 4329</a> declares this to be the
    177        * correct media type for JavaScript, but {@link #TEXT_JAVASCRIPT_UTF_8 text/javascript} may be
    178        * necessary in certain situations for compatibility.
    179        */
    180      public static final MediaType JAVASCRIPT_UTF_8 =
    181          createConstantUtf8(APPLICATION_TYPE, "javascript");
    182      public static final MediaType JSON_UTF_8 = createConstantUtf8(APPLICATION_TYPE, "json");
    183      public static final MediaType KML = createConstant(APPLICATION_TYPE, "vnd.google-earth.kml+xml");
    184      public static final MediaType KMZ = createConstant(APPLICATION_TYPE, "vnd.google-earth.kmz");
    185      public static final MediaType MBOX = createConstant(APPLICATION_TYPE, "mbox");
    186      public static final MediaType MICROSOFT_EXCEL = createConstant(APPLICATION_TYPE, "vnd.ms-excel");
    187      public static final MediaType MICROSOFT_POWERPOINT =
    188          createConstant(APPLICATION_TYPE, "vnd.ms-powerpoint");
    189      public static final MediaType MICROSOFT_WORD = createConstant(APPLICATION_TYPE, "msword");
    190      public static final MediaType OCTET_STREAM = createConstant(APPLICATION_TYPE, "octet-stream");
    191      public static final MediaType OGG_CONTAINER = createConstant(APPLICATION_TYPE, "ogg");
    192      public static final MediaType OOXML_DOCUMENT = createConstant(APPLICATION_TYPE,
    193          "vnd.openxmlformats-officedocument.wordprocessingml.document");
    194      public static final MediaType OOXML_PRESENTATION = createConstant(APPLICATION_TYPE,
    195          "vnd.openxmlformats-officedocument.presentationml.presentation");
    196      public static final MediaType OOXML_SHEET =
    197          createConstant(APPLICATION_TYPE, "vnd.openxmlformats-officedocument.spreadsheetml.sheet");
    198      public static final MediaType OPENDOCUMENT_GRAPHICS =
    199          createConstant(APPLICATION_TYPE, "vnd.oasis.opendocument.graphics");
    200      public static final MediaType OPENDOCUMENT_PRESENTATION =
    201          createConstant(APPLICATION_TYPE, "vnd.oasis.opendocument.presentation");
    202      public static final MediaType OPENDOCUMENT_SPREADSHEET =
    203          createConstant(APPLICATION_TYPE, "vnd.oasis.opendocument.spreadsheet");
    204      public static final MediaType OPENDOCUMENT_TEXT =
    205          createConstant(APPLICATION_TYPE, "vnd.oasis.opendocument.text");
    206      public static final MediaType PDF = createConstant(APPLICATION_TYPE, "pdf");
    207      public static final MediaType POSTSCRIPT = createConstant(APPLICATION_TYPE, "postscript");
    208      public static final MediaType RTF_UTF_8 = createConstantUtf8(APPLICATION_TYPE, "rtf");
    209      public static final MediaType SHOCKWAVE_FLASH = createConstant(APPLICATION_TYPE,
    210          "x-shockwave-flash");
    211      public static final MediaType SKETCHUP = createConstant(APPLICATION_TYPE, "vnd.sketchup.skp");
    212      public static final MediaType TAR = createConstant(APPLICATION_TYPE, "x-tar");
    213      public static final MediaType XHTML_UTF_8 = createConstantUtf8(APPLICATION_TYPE, "xhtml+xml");
    214      public static final MediaType ZIP = createConstant(APPLICATION_TYPE, "zip");
    215
    216      private static final ImmutableMap<MediaType, MediaType> KNOWN_TYPES =
    217          new ImmutableMap.Builder<MediaType, MediaType>()
    218              .put(ANY_TYPE, ANY_TYPE)
    219              .put(ANY_TEXT_TYPE, ANY_TEXT_TYPE)
    220              .put(ANY_IMAGE_TYPE, ANY_IMAGE_TYPE)
    221              .put(ANY_AUDIO_TYPE, ANY_AUDIO_TYPE)
    222              .put(ANY_VIDEO_TYPE, ANY_VIDEO_TYPE)
    223              .put(ANY_APPLICATION_TYPE, ANY_APPLICATION_TYPE)
    224              /* text types */
    225              .put(CACHE_MANIFEST_UTF_8, CACHE_MANIFEST_UTF_8)
    226              .put(CSS_UTF_8, CSS_UTF_8)
    227              .put(CSV_UTF_8, CSV_UTF_8)
    228              .put(HTML_UTF_8, HTML_UTF_8)
    229              .put(I_CALENDAR_UTF_8, I_CALENDAR_UTF_8)
    230              .put(PLAIN_TEXT_UTF_8, PLAIN_TEXT_UTF_8)
    231              .put(TEXT_JAVASCRIPT_UTF_8, TEXT_JAVASCRIPT_UTF_8)
    232              .put(VCARD_UTF_8, VCARD_UTF_8)
    233              .put(WML_UTF_8, WML_UTF_8)
    234              .put(XML_UTF_8, XML_UTF_8)
    235              /* image types */
    236              .put(BMP, BMP)
    237              .put(GIF, GIF)
    238              .put(ICO, ICO)
    239              .put(JPEG, JPEG)
    240              .put(PNG, PNG)
    241              .put(SVG_UTF_8, SVG_UTF_8)
    242              .put(TIFF, TIFF)
    243              .put(WEBP, WEBP)
    244              /* audio types */
    245              .put(MP4_AUDIO, MP4_AUDIO)
    246              .put(MPEG_AUDIO, MPEG_AUDIO)
    247              .put(OGG_AUDIO, OGG_AUDIO)
    248              .put(WEBM_AUDIO, WEBM_AUDIO)
    249              /* video types */
    250              .put(MP4_VIDEO, MP4_VIDEO)
    251              .put(MPEG_VIDEO, MPEG_VIDEO)
    252              .put(OGG_VIDEO, OGG_VIDEO)
    253              .put(QUICKTIME, QUICKTIME)
    254              .put(WEBM_VIDEO, WEBM_VIDEO)
    255              .put(WMV, WMV)
    256              /* application types */
    257              .put(ATOM_UTF_8, ATOM_UTF_8)
    258              .put(BZIP2, BZIP2)
    259              .put(FORM_DATA, FORM_DATA)
    260              .put(GZIP, GZIP)
    261              .put(JAVASCRIPT_UTF_8, JAVASCRIPT_UTF_8)
    262              .put(JSON_UTF_8, JSON_UTF_8)
    263              .put(KML, KML)
    264              .put(KMZ, KMZ)
    265              .put(MBOX, MBOX)
    266              .put(MICROSOFT_EXCEL, MICROSOFT_EXCEL)
    267              .put(MICROSOFT_POWERPOINT, MICROSOFT_POWERPOINT)
    268              .put(MICROSOFT_WORD, MICROSOFT_WORD)
    269              .put(OCTET_STREAM, OCTET_STREAM)
    270              .put(OGG_CONTAINER, OGG_CONTAINER)
    271              .put(OOXML_DOCUMENT, OOXML_DOCUMENT)
    272              .put(OOXML_PRESENTATION, OOXML_PRESENTATION)
    273              .put(OOXML_SHEET, OOXML_SHEET)
    274              .put(OPENDOCUMENT_GRAPHICS, OPENDOCUMENT_GRAPHICS)
    275              .put(OPENDOCUMENT_PRESENTATION, OPENDOCUMENT_PRESENTATION)
    276              .put(OPENDOCUMENT_SPREADSHEET, OPENDOCUMENT_SPREADSHEET)
    277              .put(OPENDOCUMENT_TEXT, OPENDOCUMENT_TEXT)
    278              .put(PDF, PDF)
    279              .put(POSTSCRIPT, POSTSCRIPT)
    280              .put(RTF_UTF_8, RTF_UTF_8)
    281              .put(SHOCKWAVE_FLASH, SHOCKWAVE_FLASH)
    282              .put(SKETCHUP, SKETCHUP)
    283              .put(TAR, TAR)
    284              .put(XHTML_UTF_8, XHTML_UTF_8)
    285              .put(ZIP, ZIP)
    286              .build();
    287
    288      private final String type;
    289      private final String subtype;
    290      private final ImmutableListMultimap<String, String> parameters;
    291
    292      private MediaType(String type, String subtype,
    293          ImmutableListMultimap<String, String> parameters) {
    294        this.type = type;
    295        this.subtype = subtype;
    296        this.parameters = parameters;
    297      }
    298
    299      private static MediaType createConstant(String type, String subtype) {
    300        return new MediaType(type, subtype, ImmutableListMultimap.<String, String>of());
    301      }
    302
    303      private static MediaType createConstantUtf8(String type, String subtype) {
    304        return new MediaType(type, subtype, UTF_8_CONSTANT_PARAMETERS);
    305      }
    306
    307      /** Returns the top-level media type.  For example, {@code "text"} in {@code "text/plain"}. */
    308      public String type() {
    309        return type;
    310      }
    311
    312      /** Returns the media subtype.  For example, {@code "plain"} in {@code "text/plain"}. */
    313      public String subtype() {
    314        return subtype;
    315      }
    316
    317      /** Returns a multimap containing the parameters of this media type. */
    318      public ImmutableListMultimap<String, String> parameters() {
    319        return parameters;
    320      }
    321
    322      private Map<String, ImmutableMultiset<String>> parametersAsMap() {
    323        return Maps.transformValues(parameters.asMap(),
    324            new Function<Collection<String>, ImmutableMultiset<String>>() {
    325              @Override public ImmutableMultiset<String> apply(Collection<String> input) {
    326                return ImmutableMultiset.copyOf(input);
    327              }
    328            });
    329      }
    330
    331      /**
    332       * Returns an optional charset for the value of the charset parameter if it is specified.
    333       *
    334       * @throws IllegalStateException if multiple charset values have been set for this media type
    335       * @throws IllegalCharsetNameException if a charset value is present, but illegal
    336       * @throws UnsupportedCharsetException if a charset value is present, but no support is available
    337       *     in this instance of the Java virtual machine
    338       */
    339      public Optional<Charset> charset() {
    340        ImmutableSet<String> charsetValues = ImmutableSet.copyOf(parameters.get(CHARSET_ATTRIBUTE));
    341        switch (charsetValues.size()) {
    342          case 0:
    343            return Optional.absent();
    344          case 1:
    345            return Optional.of(Charset.forName(Iterables.getOnlyElement(charsetValues)));
    346          default:
    347            throw new IllegalStateException("Multiple charset values defined: " + charsetValues);
    348        }
    349      }
    350
    351      /**
    352       * Returns a new instance with the same type and subtype as this instance, but without any
    353       * parameters.
    354       */
    355      public MediaType withoutParameters() {
    356        return parameters.isEmpty() ? this : create(type, subtype);
    357      }
    358
    359      /**
    360       * <em>Replaces</em> all parameters with the given parameters.
    361       *
    362       * @throws IllegalArgumentException if any parameter or value is invalid
    363       */
    364      public MediaType withParameters(Multimap<String, String> parameters) {
    365        return create(type, subtype, parameters);
    366      }
    367
    368      /**
    369       * <em>Replaces</em> all parameters with the given attribute with a single parameter with the
    370       * given value. If multiple parameters with the same attributes are necessary use
    371       * {@link #withParameters}. Prefer {@link #withCharset} for setting the {@code charset} parameter
    372       * when using a {@link Charset} object.
    373       *
    374       * @throws IllegalArgumentException if either {@code attribute} or {@code value} is invalid
    375       */
    376      public MediaType withParameter(String attribute, String value) {
    377        checkNotNull(attribute);
    378        checkNotNull(value);
    379        String normalizedAttribute = normalizeToken(attribute);
    380        ImmutableListMultimap.Builder<String, String> builder = ImmutableListMultimap.builder();
    381        for (Entry<String, String> entry : parameters.entries()) {
    382          String key = entry.getKey();
    383          if (!normalizedAttribute.equals(key)) {
    384            builder.put(key, entry.getValue());
    385          }
    386        }
    387        builder.put(normalizedAttribute, normalizeParameterValue(normalizedAttribute, value));
    388        MediaType mediaType = new MediaType(type, subtype, builder.build());
    389        // Return one of the constants if the media type is a known type.
    390        return Objects.firstNonNull(KNOWN_TYPES.get(mediaType), mediaType);
    391      }
    392
    393      /**
    394       * Returns a new instance with the same type and subtype as this instance, with the
    395       * {@code charset} parameter set to the {@link Charset#name name} of the given charset. Only one
    396       * {@code charset} parameter will be present on the new instance regardless of the number set on
    397       * this one.
    398       *
    399       * <p>If a charset must be specified that is not supported on this JVM (and thus is not
    400       * representable as a {@link Charset} instance, use {@link #withParameter}.
    401       */
    402      public MediaType withCharset(Charset charset) {
    403        checkNotNull(charset);
    404        return withParameter(CHARSET_ATTRIBUTE, charset.name());
    405      }
    406
    407      /** Returns true if either the type or subtype is the wildcard. */
    408      public boolean hasWildcard() {
    409        return WILDCARD.equals(type) || WILDCARD.equals(subtype);
    410      }
    411
    412      /**
    413       * Returns {@code true} if this instance falls within the range (as defined by
    414       * <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html">the HTTP Accept header</a>)
    415       * given by the argument according to three criteria:
    416       *
    417       * <ol>
    418       * <li>The type of the argument is the wildcard or equal to the type of this instance.
    419       * <li>The subtype of the argument is the wildcard or equal to the subtype of this instance.
    420       * <li>All of the parameters present in the argument are present in this instance.
    421       * </ol>
    422       *
    423       * For example: <pre>   {@code
    424       *   PLAIN_TEXT_UTF_8.is(PLAIN_TEXT_UTF_8) // true
    425       *   PLAIN_TEXT_UTF_8.is(HTML_UTF_8) // false
    426       *   PLAIN_TEXT_UTF_8.is(ANY_TYPE) // true
    427       *   PLAIN_TEXT_UTF_8.is(ANY_TEXT_TYPE) // true
    428       *   PLAIN_TEXT_UTF_8.is(ANY_IMAGE_TYPE) // false
    429       *   PLAIN_TEXT_UTF_8.is(ANY_TEXT_TYPE.withCharset(UTF_8)) // true
    430       *   PLAIN_TEXT_UTF_8.withoutParameters().is(ANY_TEXT_TYPE.withCharset(UTF_8)) // false
    431       *   PLAIN_TEXT_UTF_8.is(ANY_TEXT_TYPE.withCharset(UTF_16)) // false}</pre>
    432       *
    433       * <p>Note that while it is possible to have the same parameter declared multiple times within a
    434       * media type this method does not consider the number of occurrences of a parameter.  For
    435       * example, {@code "text/plain; charset=UTF-8"} satisfies
    436       * {@code "text/plain; charset=UTF-8; charset=UTF-8"}.
    437       */
    438      public boolean is(MediaType mediaTypeRange) {
    439        return (mediaTypeRange.type.equals(WILDCARD) || mediaTypeRange.type.equals(this.type))
    440            && (mediaTypeRange.subtype.equals(WILDCARD) || mediaTypeRange.subtype.equals(this.subtype))
    441            && this.parameters.entries().containsAll(mediaTypeRange.parameters.entries());
    442      }
    443
    444      /**
    445       * Creates a new media type with the given type and subtype.
    446       *
    447       * @throws IllegalArgumentException if type or subtype is invalid or if a wildcard is used for the
    448       * type, but not the subtype.
    449       */
    450      public static MediaType create(String type, String subtype) {
    451        return create(type, subtype, ImmutableListMultimap.<String, String>of());
    452      }
    453
    454      /**
    455       * Creates a media type with the "application" type and the given subtype.
    456       *
    457       * @throws IllegalArgumentException if subtype is invalid
    458       */
    459      static MediaType createApplicationType(String subtype) {
    460        return create(APPLICATION_TYPE, subtype);
    461      }
    462
    463      /**
    464       * Creates a media type with the "audio" type and the given subtype.
    465       *
    466       * @throws IllegalArgumentException if subtype is invalid
    467       */
    468      static MediaType createAudioType(String subtype) {
    469        return create(AUDIO_TYPE, subtype);
    470      }
    471
    472      /**
    473       * Creates a media type with the "image" type and the given subtype.
    474       *
    475       * @throws IllegalArgumentException if subtype is invalid
    476       */
    477      static MediaType createImageType(String subtype) {
    478        return create(IMAGE_TYPE, subtype);
    479      }
    480
    481      /**
    482       * Creates a media type with the "text" type and the given subtype.
    483       *
    484       * @throws IllegalArgumentException if subtype is invalid
    485       */
    486      static MediaType createTextType(String subtype) {
    487        return create(TEXT_TYPE, subtype);
    488      }
    489
    490      /**
    491       * Creates a media type with the "video" type and the given subtype.
    492       *
    493       * @throws IllegalArgumentException if subtype is invalid
    494       */
    495      static MediaType createVideoType(String subtype) {
    496        return create(VIDEO_TYPE, subtype);
    497      }
    498
    499      private static MediaType create(String type, String subtype,
    500          Multimap<String, String> parameters) {
    501        checkNotNull(type);
    502        checkNotNull(subtype);
    503        checkNotNull(parameters);
    504        String normalizedType = normalizeToken(type);
    505        String normalizedSubtype = normalizeToken(subtype);
    506        checkArgument(!WILDCARD.equals(normalizedType) || WILDCARD.equals(normalizedSubtype),
    507            "A wildcard type cannot be used with a non-wildcard subtype");
    508        ImmutableListMultimap.Builder<String, String> builder = ImmutableListMultimap.builder();
    509        for (Entry<String, String> entry : parameters.entries()) {
    510          String attribute = normalizeToken(entry.getKey());
    511          builder.put(attribute, normalizeParameterValue(attribute, entry.getValue()));
    512        }
    513        MediaType mediaType = new MediaType(normalizedType, normalizedSubtype, builder.build());
    514        // Return one of the constants if the media type is a known type.
    515        return Objects.firstNonNull(KNOWN_TYPES.get(mediaType), mediaType);
    516      }
    517
    518      private static String normalizeToken(String token) {
    519        checkArgument(TOKEN_MATCHER.matchesAllOf(token));
    520        return Ascii.toLowerCase(token);
    521      }
    522
    523      private static String normalizeParameterValue(String attribute, String value) {
    524        return CHARSET_ATTRIBUTE.equals(attribute) ? Ascii.toLowerCase(value) : value;
    525      }
    526
    527      /**
    528       * Parses a media type from its string representation.
    529       *
    530       * @throws IllegalArgumentException if the input is not parsable
    531       */
    532      public static MediaType parse(String input) {
    533        checkNotNull(input);
    534        Tokenizer tokenizer = new Tokenizer(input);
    535        try {
    536          String type = tokenizer.consumeToken(TOKEN_MATCHER);
    537          tokenizer.consumeCharacter('/');
    538          String subtype = tokenizer.consumeToken(TOKEN_MATCHER);
    539          ImmutableListMultimap.Builder<String, String> parameters = ImmutableListMultimap.builder();
    540          while (tokenizer.hasMore()) {
    541            tokenizer.consumeCharacter(';');
    542            tokenizer.consumeTokenIfPresent(LINEAR_WHITE_SPACE);
    543            String attribute = tokenizer.consumeToken(TOKEN_MATCHER);
    544            tokenizer.consumeCharacter('=');
    545            final String value;
    546            if ('"' == tokenizer.previewChar()) {
    547              tokenizer.consumeCharacter('"');
    548              StringBuilder valueBuilder = new StringBuilder();
    549              while ('"' != tokenizer.previewChar()) {
    550                if ('\\' == tokenizer.previewChar()) {
    551                  tokenizer.consumeCharacter('\\');
    552                  valueBuilder.append(tokenizer.consumeCharacter(ASCII));
    553                } else {
    554                  valueBuilder.append(tokenizer.consumeToken(QUOTED_TEXT_MATCHER));
    555                }
    556              }
    557              value = valueBuilder.toString();
    558              tokenizer.consumeCharacter('"');
    559            } else {
    560              value = tokenizer.consumeToken(TOKEN_MATCHER);
    561            }
    562            parameters.put(attribute, value);
    563          }
    564          return create(type, subtype, parameters.build());
    565        } catch (IllegalStateException e) {
    566          throw new IllegalArgumentException(e);
    567        }
    568      }
    569
    570      private static final class Tokenizer {
    571        final String input;
    572        int position = 0;
    573
    574        Tokenizer(String input) {
    575          this.input = input;
    576        }
    577
    578        String consumeTokenIfPresent(CharMatcher matcher) {
    579          checkState(hasMore());
    580          int startPosition = position;
    581          position = matcher.negate().indexIn(input, startPosition);
    582          return hasMore() ? input.substring(startPosition, position) : input.substring(startPosition);
    583        }
    584
    585        String consumeToken(CharMatcher matcher) {
    586          int startPosition = position;
    587          String token = consumeTokenIfPresent(matcher);
    588          checkState(position != startPosition);
    589          return token;
    590        }
    591
    592        char consumeCharacter(CharMatcher matcher) {
    593          checkState(hasMore());
    594          char c = previewChar();
    595          checkState(matcher.matches(c));
    596          position++;
    597          return c;
    598        }
    599
    600        char consumeCharacter(char c) {
    601          checkState(hasMore());
    602          checkState(previewChar() == c);
    603          position++;
    604          return c;
    605        }
    606
    607        char previewChar() {
    608          checkState(hasMore());
    609          return input.charAt(position);
    610        }
    611
    612        boolean hasMore() {
    613          return (position >= 0) && (position < input.length());
    614        }
    615      }
    616
    617      @Override public boolean equals(@Nullable Object obj) {
    618        if (obj == this) {
    619          return true;
    620        } else if (obj instanceof MediaType) {
    621          MediaType that = (MediaType) obj;
    622          return this.type.equals(that.type)
    623              && this.subtype.equals(that.subtype)
    624              // compare parameters regardless of order
    625              && this.parametersAsMap().equals(that.parametersAsMap());
    626        } else {
    627          return false;
    628        }
    629      }
    630
    631      @Override public int hashCode() {
    632        return Objects.hashCode(type, subtype, parametersAsMap());
    633      }
    634
    635      private static final MapJoiner PARAMETER_JOINER = Joiner.on("; ").withKeyValueSeparator("=");
    636
    637      /**
    638       * Returns the string representation of this media type in the format described in <a
    639       * href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>.
    640       */
    641      @Override public String toString() {
    642        StringBuilder builder = new StringBuilder().append(type).append('/').append(subtype);
    643        if (!parameters.isEmpty()) {
    644          builder.append("; ");
    645          Multimap<String, String> quotedParameters = Multimaps.transformValues(parameters,
    646              new Function<String, String>() {
    647                @Override public String apply(String value) {
    648                  return TOKEN_MATCHER.matchesAllOf(value) ? value : escapeAndQuote(value);
    649                }
    650              });
    651          PARAMETER_JOINER.appendTo(builder, quotedParameters.entries());
    652        }
    653        return builder.toString();
    654      }
    655
    656      private static String escapeAndQuote(String value) {
    657        StringBuilder escaped = new StringBuilder(value.length() + 16).append('"');
    658        for (char ch : value.toCharArray()) {
    659          if (ch == '\r' || ch == '\\' || ch == '"') {
    660            escaped.append('\\');
    661          }
    662          escaped.append(ch);
    663        }
    664        return escaped.append('"').toString();
    665      }
    666
    667    }