View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.juneau;
18  
19  import static org.apache.juneau.commons.utils.CollectionUtils.*;
20  import static org.apache.juneau.commons.utils.StringUtils.*;
21  import static org.apache.juneau.commons.utils.Utils.*;
22  
23  import java.util.*;
24  import java.util.function.*;
25  
26  import org.apache.http.*;
27  import org.apache.http.message.*;
28  import org.apache.juneau.annotation.*;
29  import org.apache.juneau.commons.collections.*;
30  import org.apache.juneau.commons.utils.*;
31  import org.apache.juneau.json.*;
32  
33  /**
34   * Describes a single media type used in content negotiation between an HTTP client and server, as described in
35   * Section 14.1 and 14.7 of RFC2616 (the HTTP/1.1 specification).
36   *
37   * <h5 class='section'>See Also:</h5><ul>
38   * 	<li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestCommonBasics">juneau-rest-common Basics</a>
39   * 	<li class='extlink'><a class="doclink" href="https://www.w3.org/Protocols/rfc2616/rfc2616.html">Hypertext Transfer Protocol -- HTTP/1.1</a>
40   * </ul>
41   */
42  @BeanIgnore
43  public class MediaType implements Comparable<MediaType> {
44  	/** Represents an empty media type object. */
45  	public static final MediaType EMPTY = new MediaType("/*");
46  
47  	private static final Cache<String,MediaType> CACHE = Cache.of(String.class, MediaType.class).build();
48  
49  	/** Reusable predefined media type */
50  	@SuppressWarnings("javadoc")
51  	// @formatter:off
52  	public static final MediaType
53  		CSV = of("text/csv"),
54  		HTML = of("text/html"),
55  		JSON = of("application/json"),
56  		MSGPACK = of("octal/msgpack"),
57  		PLAIN = of("text/plain"),
58  		UON = of("text/uon"),
59  		URLENCODING = of("application/x-www-form-urlencoded"),
60  		XML = of("text/xml"),
61  		XMLSOAP = of("text/xml+soap"),
62  
63  		RDF = of("text/xml+rdf"),
64  		RDFABBREV = of("text/xml+rdf+abbrev"),
65  		NTRIPLE = of("text/n-triple"),
66  		TURTLE = of("text/turtle"),
67  		N3 = of("text/n3")
68  	;
69  	// @formatter:on
70  
71  	/**
72  	 * Returns the media type for the specified string.
73  	 * The same media type strings always return the same objects so that these objects
74  	 * can be compared for equality using '=='.
75  	 *
76  	 * <h5 class='section'>Notes:</h5><ul>
77  	 * 	<li class='note'>
78  	 * 		Spaces are replaced with <js>'+'</js> characters.
79  	 * 		This gets around the issue where passing media type strings with <js>'+'</js> as HTTP GET parameters
80  	 * 		get replaced with spaces by your browser.  Since spaces aren't supported by the spec, this
81  	 * 		is doesn't break anything.
82  	 * 	<li class='note'>
83  	 * 		Anything including and following the <js>';'</js> character is ignored (e.g. <js>";charset=X"</js>).
84  	 * </ul>
85  	 *
86  	 * @param value
87  	 * 	The media type string.
88  	 * 	Will be lowercased.
89  	 * 	Returns <jk>null</jk> if input is null or empty.
90  	 * @return A cached media type object.
91  	 */
92  	public static MediaType of(String value) {
93  		return value == null ? null : CACHE.get(value, () -> new MediaType(value));
94  	}
95  
96  	/**
97  	 * Same as {@link #of(String)} but allows you to specify the parameters.
98  	 *
99  	 *
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 }