View Javadoc
1   /*
2    * Copyright 2002-2014 the original author or authors.
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  
17  package org.springframework.http;
18  
19  import java.io.Serializable;
20  import java.nio.charset.Charset;
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.Collections;
24  import java.util.Comparator;
25  import java.util.LinkedHashMap;
26  import java.util.List;
27  import java.util.Map;
28  
29  import org.springframework.util.Assert;
30  import org.springframework.util.InvalidMimeTypeException;
31  import org.springframework.util.MimeType;
32  import org.springframework.util.MimeTypeUtils;
33  import org.springframework.util.StringUtils;
34  import org.springframework.util.comparator.CompoundComparator;
35  
36  /**
37   * A sub-class of {@link MimeType} that adds support for quality parameters as defined
38   * in the HTTP specification.
39   *
40   * @author Arjen Poutsma
41   * @author Juergen Hoeller
42   * @author Rossen Stoyanchev
43   * @since 3.0
44   * @see <a href="http://tools.ietf.org/html/rfc7231#section-3.1.1.1">HTTP 1.1: Semantics
45   * and Content, section 3.1.1.1</a>
46   */
47  public class MediaType extends MimeType implements Serializable {
48  
49  	private static final long serialVersionUID = 2069937152339670231L;
50  
51  	/**
52  	 * Public constant media type that includes all media ranges (i.e. "&#42;/&#42;").
53  	 */
54  	public static final MediaType ALL;
55  
56  	/**
57  	 * A String equivalent of {@link MediaType#ALL}.
58  	 */
59  	public static final String ALL_VALUE = "*/*";
60  
61  	/**
62  	 *  Public constant media type for {@code application/atom+xml}.
63  	 */
64  	public final static MediaType APPLICATION_ATOM_XML;
65  
66  	/**
67  	 * A String equivalent of {@link MediaType#APPLICATION_ATOM_XML}.
68  	 */
69  	public final static String APPLICATION_ATOM_XML_VALUE = "application/atom+xml";
70  
71  	/**
72  	 * Public constant media type for {@code application/x-www-form-urlencoded}.
73  	 *  */
74  	public final static MediaType APPLICATION_FORM_URLENCODED;
75  
76  	/**
77  	 * A String equivalent of {@link MediaType#APPLICATION_FORM_URLENCODED}.
78  	 */
79  	public final static String APPLICATION_FORM_URLENCODED_VALUE = "application/x-www-form-urlencoded";
80  
81  	/**
82  	 * Public constant media type for {@code application/json}.
83  	 * */
84  	public final static MediaType APPLICATION_JSON;
85  
86  	/**
87  	 * A String equivalent of {@link MediaType#APPLICATION_JSON}.
88  	 */
89  	public final static String APPLICATION_JSON_VALUE = "application/json";
90  
91  	/**
92  	 * Public constant media type for {@code application/octet-stream}.
93  	 *  */
94  	public final static MediaType APPLICATION_OCTET_STREAM;
95  
96  	/**
97  	 * A String equivalent of {@link MediaType#APPLICATION_OCTET_STREAM}.
98  	 */
99  	public final static String APPLICATION_OCTET_STREAM_VALUE = "application/octet-stream";
100 
101 	/**
102 	 * Public constant media type for {@code application/xhtml+xml}.
103 	 *  */
104 	public final static MediaType APPLICATION_XHTML_XML;
105 
106 	/**
107 	 * A String equivalent of {@link MediaType#APPLICATION_XHTML_XML}.
108 	 */
109 	public final static String APPLICATION_XHTML_XML_VALUE = "application/xhtml+xml";
110 
111 	/**
112 	 * Public constant media type for {@code application/xml}.
113 	 */
114 	public final static MediaType APPLICATION_XML;
115 
116 	/**
117 	 * A String equivalent of {@link MediaType#APPLICATION_XML}.
118 	 */
119 	public final static String APPLICATION_XML_VALUE = "application/xml";
120 
121 	/**
122 	 * Public constant media type for {@code image/gif}.
123 	 */
124 	public final static MediaType IMAGE_GIF;
125 
126 	/**
127 	 * A String equivalent of {@link MediaType#IMAGE_GIF}.
128 	 */
129 	public final static String IMAGE_GIF_VALUE = "image/gif";
130 
131 	/**
132 	 * Public constant media type for {@code image/jpeg}.
133 	 */
134 	public final static MediaType IMAGE_JPEG;
135 
136 	/**
137 	 * A String equivalent of {@link MediaType#IMAGE_JPEG}.
138 	 */
139 	public final static String IMAGE_JPEG_VALUE = "image/jpeg";
140 
141 	/**
142 	 * Public constant media type for {@code image/png}.
143 	 */
144 	public final static MediaType IMAGE_PNG;
145 
146 	/**
147 	 * A String equivalent of {@link MediaType#IMAGE_PNG}.
148 	 */
149 	public final static String IMAGE_PNG_VALUE = "image/png";
150 
151 	/**
152 	 * Public constant media type for {@code multipart/form-data}.
153 	 *  */
154 	public final static MediaType MULTIPART_FORM_DATA;
155 
156 	/**
157 	 * A String equivalent of {@link MediaType#MULTIPART_FORM_DATA}.
158 	 */
159 	public final static String MULTIPART_FORM_DATA_VALUE = "multipart/form-data";
160 
161 	/**
162 	 * Public constant media type for {@code text/html}.
163 	 *  */
164 	public final static MediaType TEXT_HTML;
165 
166 	/**
167 	 * A String equivalent of {@link MediaType#TEXT_HTML}.
168 	 */
169 	public final static String TEXT_HTML_VALUE = "text/html";
170 
171 	/**
172 	 * Public constant media type for {@code text/plain}.
173 	 *  */
174 	public final static MediaType TEXT_PLAIN;
175 
176 	/**
177 	 * A String equivalent of {@link MediaType#TEXT_PLAIN}.
178 	 */
179 	public final static String TEXT_PLAIN_VALUE = "text/plain";
180 
181 	/**
182 	 * Public constant media type for {@code text/xml}.
183 	 *  */
184 	public final static MediaType TEXT_XML;
185 
186 	/**
187 	 * A String equivalent of {@link MediaType#TEXT_XML}.
188 	 */
189 	public final static String TEXT_XML_VALUE = "text/xml";
190 
191 
192 	private static final String PARAM_QUALITY_FACTOR = "q";
193 
194 
195 	static {
196 		ALL = valueOf(ALL_VALUE);
197 		APPLICATION_ATOM_XML = valueOf(APPLICATION_ATOM_XML_VALUE);
198 		APPLICATION_FORM_URLENCODED = valueOf(APPLICATION_FORM_URLENCODED_VALUE);
199 		APPLICATION_JSON = valueOf(APPLICATION_JSON_VALUE);
200 		APPLICATION_OCTET_STREAM = valueOf(APPLICATION_OCTET_STREAM_VALUE);
201 		APPLICATION_XHTML_XML = valueOf(APPLICATION_XHTML_XML_VALUE);
202 		APPLICATION_XML = valueOf(APPLICATION_XML_VALUE);
203 		IMAGE_GIF = valueOf(IMAGE_GIF_VALUE);
204 		IMAGE_JPEG = valueOf(IMAGE_JPEG_VALUE);
205 		IMAGE_PNG = valueOf(IMAGE_PNG_VALUE);
206 		MULTIPART_FORM_DATA = valueOf(MULTIPART_FORM_DATA_VALUE);
207 		TEXT_HTML = valueOf(TEXT_HTML_VALUE);
208 		TEXT_PLAIN = valueOf(TEXT_PLAIN_VALUE);
209 		TEXT_XML = valueOf(TEXT_XML_VALUE);
210 	}
211 
212 
213 	/**
214 	 * Create a new {@code MediaType} for the given primary type.
215 	 * <p>The {@linkplain #getSubtype() subtype} is set to "&#42;", parameters empty.
216 	 * @param type the primary type
217 	 * @throws IllegalArgumentException if any of the parameters contain illegal characters
218 	 */
219 	public MediaType(String type) {
220 		super(type);
221 	}
222 
223 	/**
224 	 * Create a new {@code MediaType} for the given primary type and subtype.
225 	 * <p>The parameters are empty.
226 	 * @param type the primary type
227 	 * @param subtype the subtype
228 	 * @throws IllegalArgumentException if any of the parameters contain illegal characters
229 	 */
230 	public MediaType(String type, String subtype) {
231 		super(type, subtype, Collections.<String, String>emptyMap());
232 	}
233 
234 	/**
235 	 * Create a new {@code MediaType} for the given type, subtype, and character set.
236 	 * @param type the primary type
237 	 * @param subtype the subtype
238 	 * @param charset the character set
239 	 * @throws IllegalArgumentException if any of the parameters contain illegal characters
240 	 */
241 	public MediaType(String type, String subtype, Charset charset) {
242 		super(type, subtype, charset);
243 	}
244 
245 	/**
246 	 * Create a new {@code MediaType} for the given type, subtype, and quality value.
247 	 * @param type the primary type
248 	 * @param subtype the subtype
249 	 * @param qualityValue the quality value
250 	 * @throws IllegalArgumentException if any of the parameters contain illegal characters
251 	 */
252 	public MediaType(String type, String subtype, double qualityValue) {
253 		this(type, subtype, Collections.singletonMap(PARAM_QUALITY_FACTOR, Double.toString(qualityValue)));
254 	}
255 
256 	/**
257 	 * Copy-constructor that copies the type and subtype of the given {@code MediaType},
258 	 * and allows for different parameter.
259 	 * @param other the other media type
260 	 * @param parameters the parameters, may be {@code null}
261 	 * @throws IllegalArgumentException if any of the parameters contain illegal characters
262 	 */
263 	public MediaType(MediaType other, Map<String, String> parameters) {
264 		super(other.getType(), other.getSubtype(), parameters);
265 	}
266 
267 	/**
268 	 * Create a new {@code MediaType} for the given type, subtype, and parameters.
269 	 * @param type the primary type
270 	 * @param subtype the subtype
271 	 * @param parameters the parameters, may be {@code null}
272 	 * @throws IllegalArgumentException if any of the parameters contain illegal characters
273 	 */
274 	public MediaType(String type, String subtype, Map<String, String> parameters) {
275 		super(type, subtype, parameters);
276 	}
277 
278 
279 	protected void checkParameters(String attribute, String value) {
280 		super.checkParameters(attribute, value);
281 		if (PARAM_QUALITY_FACTOR.equals(attribute)) {
282 			value = unquote(value);
283 			double d = Double.parseDouble(value);
284 			Assert.isTrue(d >= 0D && d <= 1D,
285 					"Invalid quality value \"" + value + "\": should be between 0.0 and 1.0");
286 		}
287 	}
288 
289 	/**
290 	 * Return the quality value, as indicated by a {@code q} parameter, if any.
291 	 * Defaults to {@code 1.0}.
292 	 * @return the quality factory
293 	 */
294 	public double getQualityValue() {
295 		String qualityFactory = getParameter(PARAM_QUALITY_FACTOR);
296 		return (qualityFactory != null ? Double.parseDouble(unquote(qualityFactory)) : 1D);
297 	}
298 
299 	/**
300 	 * Indicate whether this {@code MediaType} includes the given media type.
301 	 * <p>For instance, {@code text/*} includes {@code text/plain} and {@code text/html}, and {@code application/*+xml}
302 	 * includes {@code application/soap+xml}, etc. This method is <b>not</b> symmetric.
303 	 * @param other the reference media type with which to compare
304 	 * @return {@code true} if this media type includes the given media type; {@code false} otherwise
305 	 */
306 	public boolean includes(MediaType other) {
307 		return super.includes(other);
308 	}
309 
310 	/**
311 	 * Indicate whether this {@code MediaType} is compatible with the given media type.
312 	 * <p>For instance, {@code text/*} is compatible with {@code text/plain}, {@code text/html}, and vice versa.
313 	 * In effect, this method is similar to {@link #includes(MediaType)}, except that it <b>is</b> symmetric.
314 	 * @param other the reference media type with which to compare
315 	 * @return {@code true} if this media type is compatible with the given media type; {@code false} otherwise
316 	 */
317 	public boolean isCompatibleWith(MediaType other) {
318 		return super.isCompatibleWith(other);
319 	}
320 
321 	/**
322 	 * Return a replica of this instance with the quality value of the given MediaType.
323 	 * @return the same instance if the given MediaType doesn't have a quality value, or a new one otherwise
324 	 */
325 	public MediaType copyQualityValue(MediaType mediaType) {
326 		if (!mediaType.getParameters().containsKey(PARAM_QUALITY_FACTOR)) {
327 			return this;
328 		}
329 		Map<String, String> params = new LinkedHashMap<String, String>(getParameters());
330 		params.put(PARAM_QUALITY_FACTOR, mediaType.getParameters().get(PARAM_QUALITY_FACTOR));
331 		return new MediaType(this, params);
332 	}
333 
334 	/**
335 	 * Return a replica of this instance with its quality value removed.
336 	 * @return the same instance if the media type doesn't contain a quality value, or a new one otherwise
337 	 */
338 	public MediaType removeQualityValue() {
339 		if (!getParameters().containsKey(PARAM_QUALITY_FACTOR)) {
340 			return this;
341 		}
342 		Map<String, String> params = new LinkedHashMap<String, String>(getParameters());
343 		params.remove(PARAM_QUALITY_FACTOR);
344 		return new MediaType(this, params);
345 	}
346 
347 
348 	/**
349 	 * Parse the given String value into a {@code MediaType} object,
350 	 * with this method name following the 'valueOf' naming convention
351 	 * (as supported by {@link org.springframework.core.convert.ConversionService}.
352 	 * @see #parseMediaType(String)
353 	 */
354 	public static MediaType valueOf(String value) {
355 		return parseMediaType(value);
356 	}
357 
358 	/**
359 	 * Parse the given String into a single {@code MediaType}.
360 	 * @param mediaType the string to parse
361 	 * @return the media type
362 	 * @throws InvalidMediaTypeException if the string cannot be parsed
363 	 */
364 	public static MediaType parseMediaType(String mediaType) {
365 		MimeType type;
366 		try {
367 			type = MimeTypeUtils.parseMimeType(mediaType);
368 		}
369 		catch (InvalidMimeTypeException ex) {
370 			throw new InvalidMediaTypeException(ex);
371 		}
372 		try {
373 			return new MediaType(type.getType(), type.getSubtype(), type.getParameters());
374 		}
375 		catch (IllegalArgumentException ex) {
376 			throw new InvalidMediaTypeException(mediaType, ex.getMessage());
377 		}
378 	}
379 
380 
381 	/**
382 	 * Parse the given, comma-separated string into a list of {@code MediaType} objects.
383 	 * <p>This method can be used to parse an Accept or Content-Type header.
384 	 * @param mediaTypes the string to parse
385 	 * @return the list of media types
386 	 * @throws IllegalArgumentException if the string cannot be parsed
387 	 */
388 	public static List<MediaType> parseMediaTypes(String mediaTypes) {
389 		if (!StringUtils.hasLength(mediaTypes)) {
390 			return Collections.emptyList();
391 		}
392 		String[] tokens = mediaTypes.split(",\\s*");
393 		List<MediaType> result = new ArrayList<MediaType>(tokens.length);
394 		for (String token : tokens) {
395 			result.add(parseMediaType(token));
396 		}
397 		return result;
398 	}
399 
400 	/**
401 	 * Return a string representation of the given list of {@code MediaType} objects.
402 	 * <p>This method can be used to for an {@code Accept} or {@code Content-Type} header.
403 	 * @param mediaTypes the string to parse
404 	 * @return the list of media types
405 	 * @throws IllegalArgumentException if the String cannot be parsed
406 	 */
407 	public static String toString(Collection<MediaType> mediaTypes) {
408 		return MimeTypeUtils.toString(mediaTypes);
409 	}
410 
411 	/**
412 	 * Sorts the given list of {@code MediaType} objects by specificity.
413 	 * <p>Given two media types:
414 	 * <ol>
415 	 * <li>if either media type has a {@linkplain #isWildcardType() wildcard type}, then the media type without the
416 	 * wildcard is ordered before the other.</li>
417 	 * <li>if the two media types have different {@linkplain #getType() types}, then they are considered equal and
418 	 * remain their current order.</li>
419 	 * <li>if either media type has a {@linkplain #isWildcardSubtype() wildcard subtype}, then the media type without
420 	 * the wildcard is sorted before the other.</li>
421 	 * <li>if the two media types have different {@linkplain #getSubtype() subtypes}, then they are considered equal
422 	 * and remain their current order.</li>
423 	 * <li>if the two media types have different {@linkplain #getQualityValue() quality value}, then the media type
424 	 * with the highest quality value is ordered before the other.</li>
425 	 * <li>if the two media types have a different amount of {@linkplain #getParameter(String) parameters}, then the
426 	 * media type with the most parameters is ordered before the other.</li>
427 	 * </ol>
428 	 * <p>For example:
429 	 * <blockquote>audio/basic &lt; audio/* &lt; *&#047;*</blockquote>
430 	 * <blockquote>audio/* &lt; audio/*;q=0.7; audio/*;q=0.3</blockquote>
431 	 * <blockquote>audio/basic;level=1 &lt; audio/basic</blockquote>
432 	 * <blockquote>audio/basic == text/html</blockquote>
433 	 * <blockquote>audio/basic == audio/wave</blockquote>
434 	 * @param mediaTypes the list of media types to be sorted
435 	 * @see <a href="http://tools.ietf.org/html/rfc7231#section-5.3.2">HTTP 1.1: Semantics
436 	 * and Content, section 5.3.2</a>
437 	 */
438 	public static void sortBySpecificity(List<MediaType> mediaTypes) {
439 		Assert.notNull(mediaTypes, "'mediaTypes' must not be null");
440 		if (mediaTypes.size() > 1) {
441 			Collections.sort(mediaTypes, SPECIFICITY_COMPARATOR);
442 		}
443 	}
444 
445 	/**
446 	 * Sorts the given list of {@code MediaType} objects by quality value.
447 	 * <p>Given two media types:
448 	 * <ol>
449 	 * <li>if the two media types have different {@linkplain #getQualityValue() quality value}, then the media type
450 	 * with the highest quality value is ordered before the other.</li>
451 	 * <li>if either media type has a {@linkplain #isWildcardType() wildcard type}, then the media type without the
452 	 * wildcard is ordered before the other.</li>
453 	 * <li>if the two media types have different {@linkplain #getType() types}, then they are considered equal and
454 	 * remain their current order.</li>
455 	 * <li>if either media type has a {@linkplain #isWildcardSubtype() wildcard subtype}, then the media type without
456 	 * the wildcard is sorted before the other.</li>
457 	 * <li>if the two media types have different {@linkplain #getSubtype() subtypes}, then they are considered equal
458 	 * and remain their current order.</li>
459 	 * <li>if the two media types have a different amount of {@linkplain #getParameter(String) parameters}, then the
460 	 * media type with the most parameters is ordered before the other.</li>
461 	 * </ol>
462 	 * @param mediaTypes the list of media types to be sorted
463 	 * @see #getQualityValue()
464 	 */
465 	public static void sortByQualityValue(List<MediaType> mediaTypes) {
466 		Assert.notNull(mediaTypes, "'mediaTypes' must not be null");
467 		if (mediaTypes.size() > 1) {
468 			Collections.sort(mediaTypes, QUALITY_VALUE_COMPARATOR);
469 		}
470 	}
471 
472 	/**
473 	 * Sorts the given list of {@code MediaType} objects by specificity as the
474 	 * primary criteria and quality value the secondary.
475 	 * @see MediaType#sortBySpecificity(List)
476 	 * @see MediaType#sortByQualityValue(List)
477 	 */
478 	public static void sortBySpecificityAndQuality(List<MediaType> mediaTypes) {
479 		Assert.notNull(mediaTypes, "'mediaTypes' must not be null");
480 		if (mediaTypes.size() > 1) {
481 			Collections.sort(mediaTypes, new CompoundComparator<MediaType>(
482 					MediaType.SPECIFICITY_COMPARATOR, MediaType.QUALITY_VALUE_COMPARATOR));
483 		}
484 	}
485 
486 
487 	/**
488 	 * Comparator used by {@link #sortByQualityValue(List)}.
489 	 */
490 	public static final Comparator<MediaType> QUALITY_VALUE_COMPARATOR = new Comparator<MediaType>() {
491 
492 		@Override
493 		public int compare(MediaType mediaType1, MediaType mediaType2) {
494 			double quality1 = mediaType1.getQualityValue();
495 			double quality2 = mediaType2.getQualityValue();
496 			int qualityComparison = Double.compare(quality2, quality1);
497 			if (qualityComparison != 0) {
498 				return qualityComparison;  // audio/*;q=0.7 < audio/*;q=0.3
499 			}
500 			else if (mediaType1.isWildcardType() && !mediaType2.isWildcardType()) { // */* < audio/*
501 				return 1;
502 			}
503 			else if (mediaType2.isWildcardType() && !mediaType1.isWildcardType()) { // audio/* > */*
504 				return -1;
505 			}
506 			else if (!mediaType1.getType().equals(mediaType2.getType())) { // audio/basic == text/html
507 				return 0;
508 			}
509 			else { // mediaType1.getType().equals(mediaType2.getType())
510 				if (mediaType1.isWildcardSubtype() && !mediaType2.isWildcardSubtype()) { // audio/* < audio/basic
511 					return 1;
512 				}
513 				else if (mediaType2.isWildcardSubtype() && !mediaType1.isWildcardSubtype()) { // audio/basic > audio/*
514 					return -1;
515 				}
516 				else if (!mediaType1.getSubtype().equals(mediaType2.getSubtype())) { // audio/basic == audio/wave
517 					return 0;
518 				}
519 				else {
520 					int paramsSize1 = mediaType1.getParameters().size();
521 					int paramsSize2 = mediaType2.getParameters().size();
522 					return (paramsSize2 < paramsSize1 ? -1 : (paramsSize2 == paramsSize1 ? 0 : 1)); // audio/basic;level=1 < audio/basic
523 				}
524 			}
525 		}
526 	};
527 
528 
529 	/**
530 	 * Comparator used by {@link #sortBySpecificity(List)}.
531 	 */
532 	public static final Comparator<MediaType> SPECIFICITY_COMPARATOR = new SpecificityComparator<MediaType>() {
533 
534 		@Override
535 		protected int compareParameters(MediaType mediaType1, MediaType mediaType2) {
536 			double quality1 = mediaType1.getQualityValue();
537 			double quality2 = mediaType2.getQualityValue();
538 			int qualityComparison = Double.compare(quality2, quality1);
539 			if (qualityComparison != 0) {
540 				return qualityComparison;  // audio/*;q=0.7 < audio/*;q=0.3
541 			}
542 			return super.compareParameters(mediaType1, mediaType2);
543 		}
544 	};
545 
546 }