1/*
2 * Copyright (C) 2013 Square, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.squareup.okhttp;
17
18import java.nio.charset.Charset;
19import java.util.Locale;
20import java.util.regex.Matcher;
21import java.util.regex.Pattern;
22
23/**
24 * An <a href="http://tools.ietf.org/html/rfc2045">RFC 2045</a> Media Type,
25 * appropriate to describe the content type of an HTTP request or response body.
26 */
27public final class MediaType {
28  private static final String TOKEN = "([a-zA-Z0-9-!#$%&'*+.^_`{|}~]+)";
29  private static final String QUOTED = "\"([^\"]*)\"";
30  private static final Pattern TYPE_SUBTYPE = Pattern.compile(TOKEN + "/" + TOKEN);
31  private static final Pattern PARAMETER = Pattern.compile(
32      ";\\s*(?:" + TOKEN + "=(?:" + TOKEN + "|" + QUOTED + "))?");
33
34  private final String mediaType;
35  private final String type;
36  private final String subtype;
37  private final String charset;
38
39  private MediaType(String mediaType, String type, String subtype, String charset) {
40    this.mediaType = mediaType;
41    this.type = type;
42    this.subtype = subtype;
43    this.charset = charset;
44  }
45
46  /**
47   * Returns a media type for {@code string}, or null if {@code string} is not a
48   * well-formed media type.
49   */
50  public static MediaType parse(String string) {
51    Matcher typeSubtype = TYPE_SUBTYPE.matcher(string);
52    if (!typeSubtype.lookingAt()) return null;
53    String type = typeSubtype.group(1).toLowerCase(Locale.US);
54    String subtype = typeSubtype.group(2).toLowerCase(Locale.US);
55
56    String charset = null;
57    Matcher parameter = PARAMETER.matcher(string);
58    for (int s = typeSubtype.end(); s < string.length(); s = parameter.end()) {
59      parameter.region(s, string.length());
60      if (!parameter.lookingAt()) return null; // This is not a well-formed media type.
61
62      String name = parameter.group(1);
63      if (name == null || !name.equalsIgnoreCase("charset")) continue;
64      String charsetParameter = parameter.group(2) != null
65          ? parameter.group(2)  // Value is a token.
66          : parameter.group(3); // Value is a quoted string.
67      if (charset != null && !charsetParameter.equalsIgnoreCase(charset)) {
68        throw new IllegalArgumentException("Multiple different charsets: " + string);
69      }
70      charset = charsetParameter;
71    }
72
73    return new MediaType(string, type, subtype, charset);
74  }
75
76  /**
77   * Returns the high-level media type, such as "text", "image", "audio",
78   * "video", or "application".
79   */
80  public String type() {
81    return type;
82  }
83
84  /**
85   * Returns a specific media subtype, such as "plain" or "png", "mpeg",
86   * "mp4" or "xml".
87   */
88  public String subtype() {
89    return subtype;
90  }
91
92  /**
93   * Returns the charset of this media type, or null if this media type doesn't
94   * specify a charset.
95   */
96  public Charset charset() {
97    return charset != null ? Charset.forName(charset) : null;
98  }
99
100  /**
101   * Returns the charset of this media type, or {@code defaultValue} if this
102   * media type doesn't specify a charset.
103   */
104  public Charset charset(Charset defaultValue) {
105    return charset != null ? Charset.forName(charset) : defaultValue;
106  }
107
108  /**
109   * Returns the encoded media type, like "text/plain; charset=utf-8",
110   * appropriate for use in a Content-Type header.
111   */
112  @Override public String toString() {
113    return mediaType;
114  }
115
116  @Override public boolean equals(Object o) {
117    return o instanceof MediaType && ((MediaType) o).mediaType.equals(mediaType);
118  }
119
120  @Override public int hashCode() {
121    return mediaType.hashCode();
122  }
123}
124