MediaType.java 30.5 KB
Newer Older
A
Arjen Poutsma 已提交
1
/*
2
 * Copyright 2002-2012 the original author or authors.
A
Arjen Poutsma 已提交
3 4 5 6 7 8 9 10 11 12 13 14 15 16
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

17
package org.springframework.http;
A
Arjen Poutsma 已提交
18 19 20

import java.nio.charset.Charset;
import java.util.ArrayList;
21
import java.util.BitSet;
22
import java.util.Collection;
A
Arjen Poutsma 已提交
23
import java.util.Collections;
24
import java.util.Comparator;
A
Arjen Poutsma 已提交
25 26 27 28 29
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
30
import java.util.TreeSet;
A
Arjen Poutsma 已提交
31

32 33
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
34
import org.springframework.util.LinkedCaseInsensitiveMap;
35
import org.springframework.util.StringUtils;
36
import org.springframework.util.comparator.CompoundComparator;
A
Arjen Poutsma 已提交
37 38 39 40

/**
 * Represents an Internet Media Type, as defined in the HTTP specification.
 *
41 42 43
 * <p>Consists of a {@linkplain #getType() type} and a {@linkplain #getSubtype() subtype}.
 * Also has functionality to parse media types from a string using {@link #parseMediaType(String)},
 * or multiple comma-separated media types using {@link #parseMediaTypes(String)}.
A
Arjen Poutsma 已提交
44 45
 *
 * @author Arjen Poutsma
46
 * @author Juergen Hoeller
47
 * @author Rossen Stoyanchev
48
 * @since 3.0
49
 * @see <a href="http://tools.ietf.org/html/rfc2616#section-3.7">HTTP 1.1, section 3.7</a>
A
Arjen Poutsma 已提交
50
 */
51
public class MediaType implements Comparable<MediaType> {
A
Arjen Poutsma 已提交
52

53
	/**
54
	 * Public constant media type that includes all media ranges (i.e. <code>&#42;/&#42;</code>).
55 56 57
	 */
	public static final MediaType ALL;

58 59 60 61 62
	/**
	 * A String equivalent of {@link MediaType#ALL}.
	 */
	public static final String ALL_VALUE = "*/*";

63 64 65 66 67
	/**
	 *  Public constant media type for {@code application/atom+xml}.
	 */
	public final static MediaType APPLICATION_ATOM_XML;

68 69 70 71 72
	/**
	 * A String equivalent of {@link MediaType#APPLICATION_ATOM_XML}.
	 */
	public final static String APPLICATION_ATOM_XML_VALUE = "application/atom+xml";

73 74 75 76 77
	/**
	 * Public constant media type for {@code application/x-www-form-urlencoded}.
	 *  */
	public final static MediaType APPLICATION_FORM_URLENCODED;

78 79 80 81 82
	/**
	 * A String equivalent of {@link MediaType#APPLICATION_FORM_URLENCODED}.
	 */
	public final static String APPLICATION_FORM_URLENCODED_VALUE = "application/x-www-form-urlencoded";

83 84 85 86 87
	/**
	 * Public constant media type for {@code application/json}.
	 * */
	public final static MediaType APPLICATION_JSON;

88 89 90 91 92
	/**
	 * A String equivalent of {@link MediaType#APPLICATION_JSON}.
	 */
	public final static String APPLICATION_JSON_VALUE = "application/json";

93 94 95 96 97
	/**
	 * Public constant media type for {@code application/octet-stream}.
	 *  */
	public final static MediaType APPLICATION_OCTET_STREAM;

98 99 100 101 102
	/**
	 * A String equivalent of {@link MediaType#APPLICATION_OCTET_STREAM}.
	 */
	public final static String APPLICATION_OCTET_STREAM_VALUE = "application/octet-stream";

103 104 105 106
	/**
	 * Public constant media type for {@code application/xhtml+xml}.
	 *  */
	public final static MediaType APPLICATION_XHTML_XML;
107

108 109 110 111
	/**
	 * A String equivalent of {@link MediaType#APPLICATION_XHTML_XML}.
	 */
	public final static String APPLICATION_XHTML_XML_VALUE = "application/xhtml+xml";
112

A
Arjen Poutsma 已提交
113 114 115 116
	/**
	 * Public constant media type for {@code application/xml}.
	 */
	public final static MediaType APPLICATION_XML;
117

118 119 120 121
	/**
	 * A String equivalent of {@link MediaType#APPLICATION_XML}.
	 */
	public final static String APPLICATION_XML_VALUE = "application/xml";
A
Arjen Poutsma 已提交
122

123 124 125 126
	/**
	 * Public constant media type for {@code image/gif}.
	 */
	public final static MediaType IMAGE_GIF;
127

128 129 130 131
	/**
	 * A String equivalent of {@link MediaType#IMAGE_GIF}.
	 */
	public final static String IMAGE_GIF_VALUE = "image/gif";
132 133 134 135 136

	/**
	 * Public constant media type for {@code image/jpeg}.
	 */
	public final static MediaType IMAGE_JPEG;
137

138 139 140 141
	/**
	 * A String equivalent of {@link MediaType#IMAGE_JPEG}.
	 */
	public final static String IMAGE_JPEG_VALUE = "image/jpeg";
142 143 144 145 146

	/**
	 * Public constant media type for {@code image/png}.
	 */
	public final static MediaType IMAGE_PNG;
147

148 149 150 151
	/**
	 * A String equivalent of {@link MediaType#IMAGE_PNG}.
	 */
	public final static String IMAGE_PNG_VALUE = "image/png";
152 153 154 155 156

	/**
	 * Public constant media type for {@code multipart/form-data}.
	 *  */
	public final static MediaType MULTIPART_FORM_DATA;
157

158 159 160 161
	/**
	 * A String equivalent of {@link MediaType#MULTIPART_FORM_DATA}.
	 */
	public final static String MULTIPART_FORM_DATA_VALUE = "multipart/form-data";
162 163 164 165 166

	/**
	 * Public constant media type for {@code text/html}.
	 *  */
	public final static MediaType TEXT_HTML;
167

168 169 170 171
	/**
	 * A String equivalent of {@link MediaType#TEXT_HTML}.
	 */
	public final static String TEXT_HTML_VALUE = "text/html";
172 173 174 175 176

	/**
	 * Public constant media type for {@code text/plain}.
	 *  */
	public final static MediaType TEXT_PLAIN;
177

178 179 180 181
	/**
	 * A String equivalent of {@link MediaType#TEXT_PLAIN}.
	 */
	public final static String TEXT_PLAIN_VALUE = "text/plain";
182 183 184 185 186

	/**
	 * Public constant media type for {@code text/xml}.
	 *  */
	public final static MediaType TEXT_XML;
187

188 189 190 191
	/**
	 * A String equivalent of {@link MediaType#TEXT_XML}.
	 */
	public final static String TEXT_XML_VALUE = "text/xml";
192

193

194
	private static final BitSet TOKEN;
195 196

	private static final String WILDCARD_TYPE = "*";
A
Arjen Poutsma 已提交
197

198
	private static final String PARAM_QUALITY_FACTOR = "q";
A
Arjen Poutsma 已提交
199 200 201

	private static final String PARAM_CHARSET = "charset";

202

A
Arjen Poutsma 已提交
203 204 205 206 207 208
	private final String type;

	private final String subtype;

	private final Map<String, String> parameters;

209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243

	static {
		// variable names refer to RFC 2616, section 2.2
		BitSet ctl = new BitSet(128);
		for (int i=0; i <= 31; i++) {
			ctl.set(i);
		}
		ctl.set(127);

		BitSet separators = new BitSet(128);
		separators.set('(');
		separators.set(')');
		separators.set('<');
		separators.set('>');
		separators.set('@');
		separators.set(',');
		separators.set(';');
		separators.set(':');
		separators.set('\\');
		separators.set('\"');
		separators.set('/');
		separators.set('[');
		separators.set(']');
		separators.set('?');
		separators.set('=');
		separators.set('{');
		separators.set('}');
		separators.set(' ');
		separators.set('\t');

		TOKEN = new BitSet(128);
		TOKEN.set(0, 128);
		TOKEN.andNot(ctl);
		TOKEN.andNot(separators);

244 245 246 247 248 249 250 251 252 253 254 255 256 257
		ALL = MediaType.valueOf(ALL_VALUE);
		APPLICATION_ATOM_XML = MediaType.valueOf(APPLICATION_ATOM_XML_VALUE);
		APPLICATION_FORM_URLENCODED = MediaType.valueOf(APPLICATION_FORM_URLENCODED_VALUE);
		APPLICATION_JSON = MediaType.valueOf(APPLICATION_JSON_VALUE);
		APPLICATION_OCTET_STREAM = MediaType.valueOf(APPLICATION_OCTET_STREAM_VALUE);
		APPLICATION_XHTML_XML = MediaType.valueOf(APPLICATION_XHTML_XML_VALUE);
		APPLICATION_XML = MediaType.valueOf(APPLICATION_XML_VALUE);
		IMAGE_GIF = MediaType.valueOf(IMAGE_GIF_VALUE);
		IMAGE_JPEG = MediaType.valueOf(IMAGE_JPEG_VALUE);
		IMAGE_PNG = MediaType.valueOf(IMAGE_PNG_VALUE);
		MULTIPART_FORM_DATA = MediaType.valueOf(MULTIPART_FORM_DATA_VALUE);
		TEXT_HTML = MediaType.valueOf(TEXT_HTML_VALUE);
		TEXT_PLAIN = MediaType.valueOf(TEXT_PLAIN_VALUE);
		TEXT_XML = MediaType.valueOf(TEXT_XML_VALUE);
258 259
	}

260

A
Arjen Poutsma 已提交
261
	/**
A
Javadoc  
Arjen Poutsma 已提交
262
	 * Create a new {@code MediaType} for the given primary type.
263
	 * <p>The {@linkplain #getSubtype() subtype} is set to <code>&#42;</code>, parameters empty.
A
Arjen Poutsma 已提交
264
	 * @param type the primary type
265
	 * @throws IllegalArgumentException if any of the parameters contain illegal characters
A
Arjen Poutsma 已提交
266 267 268 269 270 271
	 */
	public MediaType(String type) {
		this(type, WILDCARD_TYPE);
	}

	/**
A
Javadoc  
Arjen Poutsma 已提交
272
	 * Create a new {@code MediaType} for the given primary type and subtype.
273
	 * <p>The parameters are empty.
A
Javadoc  
Arjen Poutsma 已提交
274
	 * @param type the primary type
A
Arjen Poutsma 已提交
275
	 * @param subtype the subtype
276
	 * @throws IllegalArgumentException if any of the parameters contain illegal characters
A
Arjen Poutsma 已提交
277 278 279 280 281 282
	 */
	public MediaType(String type, String subtype) {
		this(type, subtype, Collections.<String, String>emptyMap());
	}

	/**
A
Javadoc  
Arjen Poutsma 已提交
283
	 * Create a new {@code MediaType} for the given type, subtype, and character set.
284
	 * @param type the primary type
A
Arjen Poutsma 已提交
285 286
	 * @param subtype the subtype
	 * @param charSet the character set
287
	 * @throws IllegalArgumentException if any of the parameters contain illegal characters
A
Arjen Poutsma 已提交
288 289
	 */
	public MediaType(String type, String subtype, Charset charSet) {
290
		this(type, subtype, Collections.singletonMap(PARAM_CHARSET, charSet.name()));
A
Arjen Poutsma 已提交
291 292
	}

293
	/**
A
Javadoc  
Arjen Poutsma 已提交
294
	 * Create a new {@code MediaType} for the given type, subtype, and quality value.
295 296 297 298
	 *
	 * @param type the primary type
	 * @param subtype the subtype
	 * @param qualityValue the quality value
299
	 * @throws IllegalArgumentException if any of the parameters contain illegal characters
300 301
	 */
	public MediaType(String type, String subtype, double qualityValue) {
302
		this(type, subtype, Collections.singletonMap(PARAM_QUALITY_FACTOR, Double.toString(qualityValue)));
303 304
	}

305
	/**
A
Javadoc  
Arjen Poutsma 已提交
306
	 * Copy-constructor that copies the type and subtype of the given {@code MediaType},
307
	 * and allows for different parameter.
308 309 310 311 312 313 314 315
	 * @param other the other media type
	 * @param parameters the parameters, may be <code>null</code>
	 * @throws IllegalArgumentException if any of the parameters contain illegal characters
	 */
	public MediaType(MediaType other, Map<String, String> parameters) {
		this(other.getType(), other.getSubtype(), parameters);
	}

A
Arjen Poutsma 已提交
316
	/**
A
Javadoc  
Arjen Poutsma 已提交
317
	 * Create a new {@code MediaType} for the given type, subtype, and parameters.
318 319
	 * @param type the primary type
	 * @param subtype the subtype
320
	 * @param parameters the parameters, may be <code>null</code>
321
	 * @throws IllegalArgumentException if any of the parameters contain illegal characters
A
Arjen Poutsma 已提交
322 323
	 */
	public MediaType(String type, String subtype, Map<String, String> parameters) {
324 325 326 327
		Assert.hasLength(type, "'type' must not be empty");
		Assert.hasLength(subtype, "'subtype' must not be empty");
		checkToken(type);
		checkToken(subtype);
A
Arjen Poutsma 已提交
328 329
		this.type = type.toLowerCase(Locale.ENGLISH);
		this.subtype = subtype.toLowerCase(Locale.ENGLISH);
330
		if (!CollectionUtils.isEmpty(parameters)) {
331 332 333 334
			Map<String, String> m = new LinkedCaseInsensitiveMap<String>(parameters.size(), Locale.ENGLISH);
			for (Map.Entry<String, String> entry : parameters.entrySet()) {
				String attribute = entry.getKey();
				String value = entry.getValue();
335
				checkParameters(attribute, value);
336
				m.put(attribute, value);
337 338
			}
			this.parameters = Collections.unmodifiableMap(m);
A
Arjen Poutsma 已提交
339 340 341 342 343 344
		}
		else {
			this.parameters = Collections.emptyMap();
		}
	}

345 346 347 348 349 350 351 352 353 354 355 356 357 358
	/**
	 * Checks the given token string for illegal characters, as defined in RFC 2616, section 2.2.
	 * @throws IllegalArgumentException in case of illegal characters
	 * @see <a href="http://tools.ietf.org/html/rfc2616#section-2.2">HTTP 1.1, section 2.2</a>
	 */
	private void checkToken(String s) {
		for (int i=0; i < s.length(); i++ ) {
			char ch = s.charAt(i);
			if (!TOKEN.get(ch)) {
				throw new IllegalArgumentException("Invalid token character '" + ch + "' in token \"" + s + "\"");
			}
		}
	}

359 360 361 362 363
	private void checkParameters(String attribute, String value) {
		Assert.hasLength(attribute, "parameter attribute must not be empty");
		Assert.hasLength(value, "parameter value must not be empty");
		checkToken(attribute);
		if (PARAM_QUALITY_FACTOR.equals(attribute)) {
364
			value = unquote(value);
365
			double d = Double.parseDouble(value);
366 367
			Assert.isTrue(d >= 0D && d <= 1D,
					"Invalid quality value \"" + value + "\": should be between 0.0 and 1.0");
368 369
		}
		else if (PARAM_CHARSET.equals(attribute)) {
370
			value = unquote(value);
371
			Charset.forName(value);
372
		}
373 374 375 376 377 378
		else if (!isQuotedString(value)) {
			checkToken(value);
		}
	}

	private boolean isQuotedString(String s) {
379 380 381 382 383 384
		if (s.length() < 2) {
			return false;
		}
		else {
			return ((s.startsWith("\"") && s.endsWith("\"")) || (s.startsWith("'") && s.endsWith("'")));
		}
385 386
	}

387 388 389 390 391 392 393
	private String unquote(String s) {
		if (s == null) {
			return null;
		}
		return isQuotedString(s) ? s.substring(1, s.length() - 1) : s;
	}

394 395 396
	/**
	 * Return the primary type.
	 */
A
Arjen Poutsma 已提交
397
	public String getType() {
398
		return this.type;
A
Arjen Poutsma 已提交
399 400
	}

401
	/**
402
	 * Indicates whether the {@linkplain #getType() type} is the wildcard character <code>&#42;</code> or not.
403
	 */
A
Arjen Poutsma 已提交
404 405 406 407
	public boolean isWildcardType() {
		return WILDCARD_TYPE.equals(type);
	}

408 409 410
	/**
	 * Return the subtype.
	 */
A
Arjen Poutsma 已提交
411
	public String getSubtype() {
412
		return this.subtype;
A
Arjen Poutsma 已提交
413 414 415
	}

	/**
416
	 * Indicates whether the {@linkplain #getSubtype() subtype} is the wildcard character <code>&#42;</code> or not.
A
Arjen Poutsma 已提交
417 418 419 420 421 422
	 * @return whether the subtype is <code>&#42;</code>
	 */
	public boolean isWildcardSubtype() {
		return WILDCARD_TYPE.equals(subtype);
	}

423 424 425 426 427 428 429 430 431
	/**
	 * Indicates whether this media type is concrete, i.e. whether neither the type or subtype is a wildcard
	 * character <code>&#42;</code>.
	 * @return whether this media type is concrete
	 */
	public boolean isConcrete() {
		return !isWildcardType() && !isWildcardSubtype();
	}

A
Arjen Poutsma 已提交
432
	/**
433
	 * Return the character set, as indicated by a <code>charset</code> parameter, if any.
A
Arjen Poutsma 已提交
434 435 436
	 * @return the character set; or <code>null</code> if not available
	 */
	public Charset getCharSet() {
437
		String charSet = getParameter(PARAM_CHARSET);
438
		return (charSet != null ? Charset.forName(unquote(charSet)) : null);
A
Arjen Poutsma 已提交
439 440 441
	}

	/**
442 443
	 * Return the quality value, as indicated by a <code>q</code> parameter, if any.
	 * Defaults to <code>1.0</code>.
A
Arjen Poutsma 已提交
444 445 446
	 * @return the quality factory
	 */
	public double getQualityValue() {
447
		String qualityFactory = getParameter(PARAM_QUALITY_FACTOR);
448
		return (qualityFactory != null ? Double.parseDouble(unquote(qualityFactory)) : 1D);
A
Arjen Poutsma 已提交
449 450 451
	}

	/**
452
	 * Return a generic parameter value, given a parameter name.
A
Arjen Poutsma 已提交
453 454 455 456
	 * @param name the parameter name
	 * @return the parameter value; or <code>null</code> if not present
	 */
	public String getParameter(String name) {
457
		return this.parameters.get(name);
A
Arjen Poutsma 已提交
458 459
	}

460 461 462 463 464 465 466 467
	/**
	 * Return all generic parameter values.
	 * @return a read-only map, possibly empty, never <code>null</code>
	 */
	public Map<String, String> getParameters() {
	        return parameters;
	}

A
Arjen Poutsma 已提交
468
	/**
A
Javadoc  
Arjen Poutsma 已提交
469 470 471
	 * Indicate whether this {@code MediaType} includes the given media type.
	 * <p>For instance, {@code text/*} includes {@code text/plain} and {@code text/html}, and {@code application/*+xml}
	 * includes {@code application/soap+xml}, etc. This method is <b>not</b> symmetric.
A
Arjen Poutsma 已提交
472 473 474 475
	 * @param other the reference media type with which to compare
	 * @return <code>true</code> if this media type includes the given media type; <code>false</code> otherwise
	 */
	public boolean includes(MediaType other) {
476 477 478 479 480
		if (other == null) {
			return false;
		}
		if (this.isWildcardType()) {
			// */* includes anything
A
Arjen Poutsma 已提交
481 482
			return true;
		}
483 484
		else if (this.type.equals(other.type)) {
			if (this.subtype.equals(other.subtype) || this.isWildcardSubtype()) {
A
Arjen Poutsma 已提交
485 486
				return true;
			}
487 488 489 490 491 492 493 494 495 496 497
			// application/*+xml includes application/soap+xml
			int thisPlusIdx = this.subtype.indexOf('+');
			int otherPlusIdx = other.subtype.indexOf('+');
			if (thisPlusIdx != -1 && otherPlusIdx != -1) {
				String thisSubtypeNoSuffix = this.subtype.substring(0, thisPlusIdx);
				String thisSubtypeSuffix = this.subtype.substring(thisPlusIdx + 1);
				String otherSubtypeSuffix = other.subtype.substring(otherPlusIdx + 1);
				if (thisSubtypeSuffix.equals(otherSubtypeSuffix) && WILDCARD_TYPE.equals(thisSubtypeNoSuffix)) {
					return true;
				}
			}
A
Arjen Poutsma 已提交
498
		}
499 500 501 502
		return false;
	}

	/**
A
Javadoc  
Arjen Poutsma 已提交
503
	 * Indicate whether this {@code MediaType} is compatible with the given media type.
504
	 * <p>For instance, {@code text/*} is compatible with {@code text/plain}, {@code text/html}, and vice versa.
A
Javadoc  
Arjen Poutsma 已提交
505
	 * In effect, this method is similar to {@link #includes(MediaType)}, except that it <b>is</b> symmetric.
506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536
	 * @param other the reference media type with which to compare
	 * @return <code>true</code> if this media type is compatible with the given media type; <code>false</code> otherwise
	 */
	public boolean isCompatibleWith(MediaType other) {
		if (other == null) {
			return false;
		}
		if (isWildcardType() || other.isWildcardType()) {
			return true;
		}
		else if (this.type.equals(other.type)) {
			if (this.subtype.equals(other.subtype) || this.isWildcardSubtype() || other.isWildcardSubtype()) {
				return true;
			}
			// application/*+xml is compatible with application/soap+xml, and vice-versa
			int thisPlusIdx = this.subtype.indexOf('+');
			int otherPlusIdx = other.subtype.indexOf('+');
			if (thisPlusIdx != -1 && otherPlusIdx != -1) {
				String thisSubtypeNoSuffix = this.subtype.substring(0, thisPlusIdx);
				String otherSubtypeNoSuffix = other.subtype.substring(0, otherPlusIdx);

				String thisSubtypeSuffix = this.subtype.substring(thisPlusIdx + 1);
				String otherSubtypeSuffix = other.subtype.substring(otherPlusIdx + 1);

				if (thisSubtypeSuffix.equals(otherSubtypeSuffix) &&
						(WILDCARD_TYPE.equals(thisSubtypeNoSuffix) || WILDCARD_TYPE.equals(otherSubtypeNoSuffix))) {
					return true;
				}
			}
		}
		return false;
A
Arjen Poutsma 已提交
537 538
	}

539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564
	/**
	 * Return a replica of this instance with the quality value of the given MediaType.
	 * @return the same instance if the given MediaType doesn't have a quality value, or a new one otherwise
	 */
	public MediaType copyQualityValue(MediaType mediaType) {
		if (!mediaType.parameters.containsKey(PARAM_QUALITY_FACTOR)) {
			return this;
		}
		Map<String, String> params = new LinkedHashMap<String, String>(this.parameters);
		params.put(PARAM_QUALITY_FACTOR, mediaType.parameters.get(PARAM_QUALITY_FACTOR));
		return new MediaType(this, params);
	}

	/**
	 * Return a replica of this instance with its quality value removed.
	 * @return the same instance if the media type doesn't contain a quality value, or a new one otherwise
	 */
	public MediaType removeQualityValue() {
		if (!this.parameters.containsKey(PARAM_QUALITY_FACTOR)) {
			return this;
		}
		Map<String, String> params = new LinkedHashMap<String, String>(this.parameters);
		params.remove(PARAM_QUALITY_FACTOR);
		return new MediaType(this, params);
	}

565
	/**
A
Javadoc  
Arjen Poutsma 已提交
566
	 * Compares this {@code MediaType} to another alphabetically.
567 568 569 570
	 * @param other media type to compare to
	 * @see #sortBySpecificity(List)
	 */
	public int compareTo(MediaType other) {
571 572 573 574 575 576 577 578 579 580 581 582
		int comp = this.type.compareToIgnoreCase(other.type);
		if (comp != 0) {
			return comp;
		}
		comp = this.subtype.compareToIgnoreCase(other.subtype);
		if (comp != 0) {
			return comp;
		}
		comp = this.parameters.size() - other.parameters.size();
		if (comp != 0) {
			return comp;
		}
583 584 585 586 587 588 589 590 591
		TreeSet<String> thisAttributes = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
		thisAttributes.addAll(this.parameters.keySet());
		TreeSet<String> otherAttributes = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
		otherAttributes.addAll(other.parameters.keySet());
		Iterator<String> thisAttributesIterator = thisAttributes.iterator();
		Iterator<String> otherAttributesIterator = otherAttributes.iterator();
		while (thisAttributesIterator.hasNext()) {
			String thisAttribute = thisAttributesIterator.next();
			String otherAttribute = otherAttributesIterator.next();
592 593 594 595 596 597 598 599 600 601 602 603 604 605 606
			comp = thisAttribute.compareToIgnoreCase(otherAttribute);
			if (comp != 0) {
				return comp;
			}
			String thisValue = this.parameters.get(thisAttribute);
			String otherValue = other.parameters.get(otherAttribute);
			if (otherValue == null) {
				otherValue = "";
			}
			comp = thisValue.compareTo(otherValue);
			if (comp != 0) {
				return comp;
			}
		}
		return 0;
607 608
	}

A
Arjen Poutsma 已提交
609
	@Override
610 611
	public boolean equals(Object other) {
		if (this == other) {
A
Arjen Poutsma 已提交
612 613
			return true;
		}
614 615
		if (!(other instanceof MediaType)) {
			return false;
A
Arjen Poutsma 已提交
616
		}
617
		MediaType otherType = (MediaType) other;
618
		return (this.type.equalsIgnoreCase(otherType.type) && this.subtype.equalsIgnoreCase(otherType.subtype) &&
619
				this.parameters.equals(otherType.parameters));
A
Arjen Poutsma 已提交
620 621 622 623
	}

	@Override
	public int hashCode() {
624 625 626
		int result = this.type.hashCode();
		result = 31 * result + this.subtype.hashCode();
		result = 31 * result + this.parameters.hashCode();
A
Arjen Poutsma 已提交
627 628 629 630 631 632 633 634 635 636
		return result;
	}

	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		appendTo(builder);
		return builder.toString();
	}

637 638 639 640
	private void appendTo(StringBuilder builder) {
		builder.append(this.type);
		builder.append('/');
		builder.append(this.subtype);
641 642 643
		appendTo(this.parameters, builder);
	}

644
	private void appendTo(Map<String, String> map, StringBuilder builder) {
645
		for (Map.Entry<String, String> entry : map.entrySet()) {
646 647 648 649 650 651 652
			builder.append(';');
			builder.append(entry.getKey());
			builder.append('=');
			builder.append(entry.getValue());
		}
	}

653 654

	/**
A
Javadoc  
Arjen Poutsma 已提交
655
	 * Parse the given String value into a {@code MediaType} object,
656 657 658 659 660 661 662 663
	 * with this method name following the 'valueOf' naming convention
	 * (as supported by {@link org.springframework.core.convert.ConversionService}.
	 * @see #parseMediaType(String)
	 */
	public static MediaType valueOf(String value) {
		return parseMediaType(value);
	}

664
	/**
A
Javadoc  
Arjen Poutsma 已提交
665
	 * Parse the given String into a single {@code MediaType}.
666 667 668 669 670 671 672 673 674 675 676 677 678
	 * @param mediaType the string to parse
	 * @return the media type
	 * @throws IllegalArgumentException if the string cannot be parsed
	 */
	public static MediaType parseMediaType(String mediaType) {
		Assert.hasLength(mediaType, "'mediaType' must not be empty");
		String[] parts = StringUtils.tokenizeToStringArray(mediaType, ";");

		String fullType = parts[0].trim();
		// java.net.HttpURLConnection returns a *; q=.2 Accept header
		if (WILDCARD_TYPE.equals(fullType)) {
			fullType = "*/*";
		}
679
		int subIndex = fullType.indexOf('/');
680 681 682 683 684 685
		if (subIndex == -1) {
			throw new IllegalArgumentException("\"" + mediaType + "\" does not contain '/'");
		}
		if (subIndex == fullType.length() - 1) {
			throw new IllegalArgumentException("\"" + mediaType + "\" does not contain subtype after '/'");
		}
686 687
		String type = fullType.substring(0, subIndex);
		String subtype = fullType.substring(subIndex + 1, fullType.length());
688 689 690
		if (WILDCARD_TYPE.equals(type) && !WILDCARD_TYPE.equals(subtype)) {
			throw new IllegalArgumentException("A wildcard type is legal only in '*/*' (all media types).");
		}
691 692 693 694 695

		Map<String, String> parameters = null;
		if (parts.length > 1) {
			parameters = new LinkedHashMap<String, String>(parts.length - 1);
			for (int i = 1; i < parts.length; i++) {
696 697
				String parameter = parts[i];
				int eqIndex = parameter.indexOf('=');
698
				if (eqIndex != -1) {
699 700 701
					String attribute = parameter.substring(0, eqIndex);
					String value = parameter.substring(eqIndex + 1, parameter.length());
					parameters.put(attribute, value);
702 703 704 705
				}
			}
		}

706 707 708
		return new MediaType(type, subtype, parameters);
	}

709

A
Arjen Poutsma 已提交
710
	/**
A
Javadoc  
Arjen Poutsma 已提交
711
	 * Parse the given, comma-separated string into a list of {@code MediaType} objects.
712
	 * <p>This method can be used to parse an Accept or Content-Type header.
A
Arjen Poutsma 已提交
713 714 715 716
	 * @param mediaTypes the string to parse
	 * @return the list of media types
	 * @throws IllegalArgumentException if the string cannot be parsed
	 */
717
	public static List<MediaType> parseMediaTypes(String mediaTypes) {
718 719 720
		if (!StringUtils.hasLength(mediaTypes)) {
			return Collections.emptyList();
		}
721 722 723 724 725 726 727 728 729
		String[] tokens = mediaTypes.split(",\\s*");
		List<MediaType> result = new ArrayList<MediaType>(tokens.length);
		for (String token : tokens) {
			result.add(parseMediaType(token));
		}
		return result;
	}

	/**
A
Javadoc  
Arjen Poutsma 已提交
730
	 * Return a string representation of the given list of {@code MediaType} objects.
731
	 * <p>This method can be used to for an {@code Accept} or {@code Content-Type} header.
732 733 734 735 736
	 * @param mediaTypes the string to parse
	 * @return the list of media types
	 * @throws IllegalArgumentException if the String cannot be parsed
	 */
	public static String toString(Collection<MediaType> mediaTypes) {
A
Arjen Poutsma 已提交
737 738 739 740 741
		StringBuilder builder = new StringBuilder();
		for (Iterator<MediaType> iterator = mediaTypes.iterator(); iterator.hasNext();) {
			MediaType mediaType = iterator.next();
			mediaType.appendTo(builder);
			if (iterator.hasNext()) {
A
Arjen Poutsma 已提交
742
				builder.append(", ");
A
Arjen Poutsma 已提交
743 744 745 746 747
			}
		}
		return builder.toString();
	}

748
	/**
A
Javadoc  
Arjen Poutsma 已提交
749
	 * Sorts the given list of {@code MediaType} objects by specificity.
750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780
	 * <p>Given two media types:
	 * <ol>
	 *   <li>if either media type has a {@linkplain #isWildcardType() wildcard type}, then the media type without the
	 *   wildcard is ordered before the other.</li>
	 *   <li>if the two media types have different {@linkplain #getType() types}, then they are considered equal and
	 *   remain their current order.</li>
	 *   <li>if either media type has a {@linkplain #isWildcardSubtype() wildcard subtype}, then the media type without
	 *   the wildcard is sorted before the other.</li>
	 *   <li>if the two media types have different {@linkplain #getSubtype() subtypes}, then they are considered equal
	 *   and remain their current order.</li>
	 *   <li>if the two media types have different {@linkplain #getQualityValue() quality value}, then the media type
	 *   with the highest quality value is ordered before the other.</li>
	 *   <li>if the two media types have a different amount of {@linkplain #getParameter(String) parameters}, then the
	 *   media type with the most parameters is ordered before the other.</li>
	 * </ol>
	 * <p>For example:
	 * <blockquote>audio/basic &lt; audio/* &lt; *&#047;*</blockquote>
	 * <blockquote>audio/* &lt; audio/*;q=0.7; audio/*;q=0.3</blockquote>
	 * <blockquote>audio/basic;level=1 &lt; audio/basic</blockquote>
	 * <blockquote>audio/basic == text/html</blockquote>
	 * <blockquote>audio/basic == audio/wave</blockquote>
	 * @param mediaTypes the list of media types to be sorted
	 * @see <a href="http://tools.ietf.org/html/rfc2616#section-14.1">HTTP 1.1, section 14.1</a>
	 */
	public static void sortBySpecificity(List<MediaType> mediaTypes) {
		Assert.notNull(mediaTypes, "'mediaTypes' must not be null");
		if (mediaTypes.size() > 1) {
			Collections.sort(mediaTypes, SPECIFICITY_COMPARATOR);
		}
	}

781
	/**
A
Javadoc  
Arjen Poutsma 已提交
782
	 * Sorts the given list of {@code MediaType} objects by quality value.
783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807
	 * <p>Given two media types:
	 * <ol>
	 *   <li>if the two media types have different {@linkplain #getQualityValue() quality value}, then the media type
	 *   with the highest quality value is ordered before the other.</li>
	 *   <li>if either media type has a {@linkplain #isWildcardType() wildcard type}, then the media type without the
	 *   wildcard is ordered before the other.</li>
	 *   <li>if the two media types have different {@linkplain #getType() types}, then they are considered equal and
	 *   remain their current order.</li>
	 *   <li>if either media type has a {@linkplain #isWildcardSubtype() wildcard subtype}, then the media type without
	 *   the wildcard is sorted before the other.</li>
	 *   <li>if the two media types have different {@linkplain #getSubtype() subtypes}, then they are considered equal
	 *   and remain their current order.</li>
	 *   <li>if the two media types have a different amount of {@linkplain #getParameter(String) parameters}, then the
	 *   media type with the most parameters is ordered before the other.</li>
	 * </ol>
	 * @param mediaTypes the list of media types to be sorted
	 * @see #getQualityValue()
	 */
	public static void sortByQualityValue(List<MediaType> mediaTypes) {
		Assert.notNull(mediaTypes, "'mediaTypes' must not be null");
		if (mediaTypes.size() > 1) {
			Collections.sort(mediaTypes, QUALITY_VALUE_COMPARATOR);
		}
	}

808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823
	/**
	 * Sorts the given list of {@code MediaType} objects by specificity as the
	 * primary criteria and quality value the secondary.
	 * @see MediaType#sortBySpecificity(List)
	 * @see MediaType#sortByQualityValue(List)
	 */
	public static void sortBySpecificityAndQuality(List<MediaType> mediaTypes) {
		Assert.notNull(mediaTypes, "'mediaTypes' must not be null");
		if (mediaTypes.size() > 1) {
			Comparator<?>[] comparators = new Comparator[2];
			comparators[0] = MediaType.SPECIFICITY_COMPARATOR;
			comparators[1] = MediaType.QUALITY_VALUE_COMPARATOR;
			Collections.sort(mediaTypes, new CompoundComparator<MediaType>(comparators));
		}
	}

824

825 826 827 828
	/**
	 * Comparator used by {@link #sortBySpecificity(List)}.
	 */
	public static final Comparator<MediaType> SPECIFICITY_COMPARATOR = new Comparator<MediaType>() {
829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855

		public int compare(MediaType mediaType1, MediaType mediaType2) {
			if (mediaType1.isWildcardType() && !mediaType2.isWildcardType()) { // */* < audio/*
				return 1;
			}
			else if (mediaType2.isWildcardType() && !mediaType1.isWildcardType()) { // audio/* > */*
				return -1;
			}
			else if (!mediaType1.getType().equals(mediaType2.getType())) { // audio/basic == text/html
				return 0;
			}
			else { // mediaType1.getType().equals(mediaType2.getType())
				if (mediaType1.isWildcardSubtype() && !mediaType2.isWildcardSubtype()) { // audio/* < audio/basic
					return 1;
				}
				else if (mediaType2.isWildcardSubtype() && !mediaType1.isWildcardSubtype()) { // audio/basic > audio/*
					return -1;
				}
				else if (!mediaType1.getSubtype().equals(mediaType2.getSubtype())) { // audio/basic == audio/wave
					return 0;
				}
				else { // mediaType2.getSubtype().equals(mediaType2.getSubtype())
					double quality1 = mediaType1.getQualityValue();
					double quality2 = mediaType2.getQualityValue();
					int qualityComparison = Double.compare(quality2, quality1);
					if (qualityComparison != 0) {
						return qualityComparison;  // audio/*;q=0.7 < audio/*;q=0.3
856 857
					}
					else {
858 859 860 861 862 863 864 865 866
						int paramsSize1 = mediaType1.parameters.size();
						int paramsSize2 = mediaType2.parameters.size();
						return (paramsSize2 < paramsSize1 ? -1 : (paramsSize2 == paramsSize1 ? 0 : 1)); // audio/basic;level=1 < audio/basic
					}
				}
			}
		}
	};

867

868 869 870 871
	/**
	 * Comparator used by {@link #sortByQualityValue(List)}.
	 */
	public static final Comparator<MediaType> QUALITY_VALUE_COMPARATOR = new Comparator<MediaType>() {
872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897

		public int compare(MediaType mediaType1, MediaType mediaType2) {
			double quality1 = mediaType1.getQualityValue();
			double quality2 = mediaType2.getQualityValue();
			int qualityComparison = Double.compare(quality2, quality1);
			if (qualityComparison != 0) {
				return qualityComparison;  // audio/*;q=0.7 < audio/*;q=0.3
			}
			else if (mediaType1.isWildcardType() && !mediaType2.isWildcardType()) { // */* < audio/*
				return 1;
			}
			else if (mediaType2.isWildcardType() && !mediaType1.isWildcardType()) { // audio/* > */*
				return -1;
			}
			else if (!mediaType1.getType().equals(mediaType2.getType())) { // audio/basic == text/html
				return 0;
			}
			else { // mediaType1.getType().equals(mediaType2.getType())
				if (mediaType1.isWildcardSubtype() && !mediaType2.isWildcardSubtype()) { // audio/* < audio/basic
					return 1;
				}
				else if (mediaType2.isWildcardSubtype() && !mediaType1.isWildcardSubtype()) { // audio/basic > audio/*
					return -1;
				}
				else if (!mediaType1.getSubtype().equals(mediaType2.getSubtype())) { // audio/basic == audio/wave
					return 0;
898 899
				}
				else {
900 901 902 903 904 905 906
					int paramsSize1 = mediaType1.parameters.size();
					int paramsSize2 = mediaType2.parameters.size();
					return (paramsSize2 < paramsSize1 ? -1 : (paramsSize2 == paramsSize1 ? 0 : 1)); // audio/basic;level=1 < audio/basic
				}
			}
		}
	};
907

A
Arjen Poutsma 已提交
908
}