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  */

16 package okhttp3;
17
18 import java.nio.charset.Charset;
19 import java.util.Locale;
20 import java.util.regex.Matcher;
21 import java.util.regex.Pattern;
22 import javax.annotation.Nullable;
23
24 /**
25  * An <a href="http://tools.ietf.org/html/rfc2045">RFC 2045</a> Media Type, appropriate to describe
26  * the content type of an HTTP request or response body.
27  */

28 public final class MediaType {
29   private static final String TOKEN = "([a-zA-Z0-9-!#$%&'*+.^_`{|}~]+)";
30   private static final String QUOTED = "\"([^\"]*)\"";
31   private static final Pattern TYPE_SUBTYPE = Pattern.compile(TOKEN + "/" + TOKEN);
32   private static final Pattern PARAMETER = Pattern.compile(
33       ";\\s*(?:" + TOKEN + "=(?:" + TOKEN + "|" + QUOTED + "))?");
34
35   private final String mediaType;
36   private final String type;
37   private final String subtype;
38   private final @Nullable String charset;
39
40   private MediaType(String mediaType, String type, String subtype, @Nullable String charset) {
41     this.mediaType = mediaType;
42     this.type = type;
43     this.subtype = subtype;
44     this.charset = charset;
45   }
46
47   /**
48    * Returns a media type for {@code string}.
49    *
50    * @throws IllegalArgumentException if {@code string} is not a well-formed media type.
51    */

52   public static MediaType get(String string) {
53     Matcher typeSubtype = TYPE_SUBTYPE.matcher(string);
54     if (!typeSubtype.lookingAt()) {
55       throw new IllegalArgumentException("No subtype found for: \"" + string + '"');
56     }
57     String type = typeSubtype.group(1).toLowerCase(Locale.US);
58     String subtype = typeSubtype.group(2).toLowerCase(Locale.US);
59
60     String charset = null;
61     Matcher parameter = PARAMETER.matcher(string);
62     for (int s = typeSubtype.end(); s < string.length(); s = parameter.end()) {
63       parameter.region(s, string.length());
64       if (!parameter.lookingAt()) {
65         throw new IllegalArgumentException("Parameter is not formatted correctly: \""
66             + string.substring(s)
67             + "\" for: \""
68             + string
69             + '"');
70       }
71
72       String name = parameter.group(1);
73       if (name == null || !name.equalsIgnoreCase("charset")) continue;
74       String charsetParameter;
75       String token = parameter.group(2);
76       if (token != null) {
77         // If the token is 'single-quoted' it's invalid! But we're lenient and strip the quotes.
78         charsetParameter = (token.startsWith("'") && token.endsWith("'") && token.length() > 2)
79             ? token.substring(1, token.length() - 1)
80             : token;
81       } else {
82         // Value is "double-quoted". That's valid and our regex group already strips the quotes.
83         charsetParameter = parameter.group(3);
84       }
85       if (charset != null && !charsetParameter.equalsIgnoreCase(charset)) {
86         throw new IllegalArgumentException("Multiple charsets defined: \""
87             + charset
88             + "\" and: \""
89             + charsetParameter
90             + "\" for: \""
91             + string
92             + '"');
93       }
94       charset = charsetParameter;
95     }
96
97     return new MediaType(string, type, subtype, charset);
98   }
99
100   /**
101    * Returns a media type for {@code string}, or null if {@code string} is not a well-formed media
102    * type.
103    */

104   public static @Nullable MediaType parse(String string) {
105     try {
106       return get(string);
107     } catch (IllegalArgumentException ignored) {
108       return null;
109     }
110   }
111
112   /**
113    * Returns the high-level media type, such as "text""image""audio""video", or
114    * "application".
115    */

116   public String type() {
117     return type;
118   }
119
120   /**
121    * Returns a specific media subtype, such as "plain" or "png""mpeg""mp4" or "xml".
122    */

123   public String subtype() {
124     return subtype;
125   }
126
127   /**
128    * Returns the charset of this media type, or null if this media type doesn't specify a charset.
129    */

130   public @Nullable Charset charset() {
131     return charset(null);
132   }
133
134   /**
135    * Returns the charset of this media type, or {@code defaultValue} if either this media type
136    * doesn't specify a charset, of it its charset is unsupported by the current runtime.
137    */

138   public @Nullable Charset charset(@Nullable Charset defaultValue) {
139     try {
140       return charset != null ? Charset.forName(charset) : defaultValue;
141     } catch (IllegalArgumentException e) {
142       return defaultValue; // This charset is invalid or unsupported. Give up.
143     }
144   }
145
146   /**
147    * Returns the encoded media type, like "text/plain; charset=utf-8", appropriate for use in a
148    * Content-Type header.
149    */

150   @Override public String toString() {
151     return mediaType;
152   }
153
154   @Override public boolean equals(@Nullable Object other) {
155     return other instanceof MediaType && ((MediaType) other).mediaType.equals(mediaType);
156   }
157
158   @Override public int hashCode() {
159     return mediaType.hashCode();
160   }
161 }
162