001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.util; 018 019import java.io.IOException; 020import java.io.ObjectInputStream; 021import java.io.Serializable; 022import java.nio.charset.Charset; 023import java.util.BitSet; 024import java.util.Collection; 025import java.util.Collections; 026import java.util.Comparator; 027import java.util.Iterator; 028import java.util.LinkedHashMap; 029import java.util.List; 030import java.util.Locale; 031import java.util.Map; 032import java.util.TreeSet; 033 034import org.springframework.lang.Nullable; 035 036/** 037 * Represents a MIME Type, as originally defined in RFC 2046 and subsequently 038 * used in other Internet protocols including HTTP. 039 * 040 * <p>This class, however, does not contain support for the q-parameters used 041 * in HTTP content negotiation. Those can be found in the subclass 042 * {@code org.springframework.http.MediaType} in the {@code spring-web} module. 043 * 044 * <p>Consists of a {@linkplain #getType() type} and a {@linkplain #getSubtype() subtype}. 045 * Also has functionality to parse MIME Type values from a {@code String} using 046 * {@link #valueOf(String)}. For more parsing options see {@link MimeTypeUtils}. 047 * 048 * @author Arjen Poutsma 049 * @author Juergen Hoeller 050 * @author Rossen Stoyanchev 051 * @author Sam Brannen 052 * @since 4.0 053 * @see MimeTypeUtils 054 */ 055public class MimeType implements Comparable<MimeType>, Serializable { 056 057 private static final long serialVersionUID = 4085923477777865903L; 058 059 060 protected static final String WILDCARD_TYPE = "*"; 061 062 private static final String PARAM_CHARSET = "charset"; 063 064 private static final BitSet TOKEN; 065 066 static { 067 // variable names refer to RFC 2616, section 2.2 068 BitSet ctl = new BitSet(128); 069 for (int i = 0; i <= 31; i++) { 070 ctl.set(i); 071 } 072 ctl.set(127); 073 074 BitSet separators = new BitSet(128); 075 separators.set('('); 076 separators.set(')'); 077 separators.set('<'); 078 separators.set('>'); 079 separators.set('@'); 080 separators.set(','); 081 separators.set(';'); 082 separators.set(':'); 083 separators.set('\\'); 084 separators.set('\"'); 085 separators.set('/'); 086 separators.set('['); 087 separators.set(']'); 088 separators.set('?'); 089 separators.set('='); 090 separators.set('{'); 091 separators.set('}'); 092 separators.set(' '); 093 separators.set('\t'); 094 095 TOKEN = new BitSet(128); 096 TOKEN.set(0, 128); 097 TOKEN.andNot(ctl); 098 TOKEN.andNot(separators); 099 } 100 101 102 private final String type; 103 104 private final String subtype; 105 106 private final Map<String, String> parameters; 107 108 @Nullable 109 private transient Charset resolvedCharset; 110 111 @Nullable 112 private volatile String toStringValue; 113 114 115 /** 116 * Create a new {@code MimeType} for the given primary type. 117 * <p>The {@linkplain #getSubtype() subtype} is set to <code>"*"</code>, 118 * and the parameters are empty. 119 * @param type the primary type 120 * @throws IllegalArgumentException if any of the parameters contains illegal characters 121 */ 122 public MimeType(String type) { 123 this(type, WILDCARD_TYPE); 124 } 125 126 /** 127 * Create a new {@code MimeType} for the given primary type and subtype. 128 * <p>The parameters are empty. 129 * @param type the primary type 130 * @param subtype the subtype 131 * @throws IllegalArgumentException if any of the parameters contains illegal characters 132 */ 133 public MimeType(String type, String subtype) { 134 this(type, subtype, Collections.emptyMap()); 135 } 136 137 /** 138 * Create a new {@code MimeType} for the given type, subtype, and character set. 139 * @param type the primary type 140 * @param subtype the subtype 141 * @param charset the character set 142 * @throws IllegalArgumentException if any of the parameters contains illegal characters 143 */ 144 public MimeType(String type, String subtype, Charset charset) { 145 this(type, subtype, Collections.singletonMap(PARAM_CHARSET, charset.name())); 146 this.resolvedCharset = charset; 147 } 148 149 /** 150 * Copy-constructor that copies the type, subtype, parameters of the given {@code MimeType}, 151 * and allows to set the specified character set. 152 * @param other the other MimeType 153 * @param charset the character set 154 * @throws IllegalArgumentException if any of the parameters contains illegal characters 155 * @since 4.3 156 */ 157 public MimeType(MimeType other, Charset charset) { 158 this(other.getType(), other.getSubtype(), addCharsetParameter(charset, other.getParameters())); 159 this.resolvedCharset = charset; 160 } 161 162 /** 163 * Copy-constructor that copies the type and subtype of the given {@code MimeType}, 164 * and allows for different parameter. 165 * @param other the other MimeType 166 * @param parameters the parameters (may be {@code null}) 167 * @throws IllegalArgumentException if any of the parameters contains illegal characters 168 */ 169 public MimeType(MimeType other, @Nullable Map<String, String> parameters) { 170 this(other.getType(), other.getSubtype(), parameters); 171 } 172 173 /** 174 * Create a new {@code MimeType} for the given type, subtype, and parameters. 175 * @param type the primary type 176 * @param subtype the subtype 177 * @param parameters the parameters (may be {@code null}) 178 * @throws IllegalArgumentException if any of the parameters contains illegal characters 179 */ 180 public MimeType(String type, String subtype, @Nullable Map<String, String> parameters) { 181 Assert.hasLength(type, "'type' must not be empty"); 182 Assert.hasLength(subtype, "'subtype' must not be empty"); 183 checkToken(type); 184 checkToken(subtype); 185 this.type = type.toLowerCase(Locale.ENGLISH); 186 this.subtype = subtype.toLowerCase(Locale.ENGLISH); 187 if (!CollectionUtils.isEmpty(parameters)) { 188 Map<String, String> map = new LinkedCaseInsensitiveMap<>(parameters.size(), Locale.ENGLISH); 189 parameters.forEach((parameter, value) -> { 190 checkParameters(parameter, value); 191 map.put(parameter, value); 192 }); 193 this.parameters = Collections.unmodifiableMap(map); 194 } 195 else { 196 this.parameters = Collections.emptyMap(); 197 } 198 } 199 200 /** 201 * Checks the given token string for illegal characters, as defined in RFC 2616, 202 * section 2.2. 203 * @throws IllegalArgumentException in case of illegal characters 204 * @see <a href="https://tools.ietf.org/html/rfc2616#section-2.2">HTTP 1.1, section 2.2</a> 205 */ 206 private void checkToken(String token) { 207 for (int i = 0; i < token.length(); i++) { 208 char ch = token.charAt(i); 209 if (!TOKEN.get(ch)) { 210 throw new IllegalArgumentException("Invalid token character '" + ch + "' in token \"" + token + "\""); 211 } 212 } 213 } 214 215 protected void checkParameters(String parameter, String value) { 216 Assert.hasLength(parameter, "'parameter' must not be empty"); 217 Assert.hasLength(value, "'value' must not be empty"); 218 checkToken(parameter); 219 if (PARAM_CHARSET.equals(parameter)) { 220 if (this.resolvedCharset == null) { 221 this.resolvedCharset = Charset.forName(unquote(value)); 222 } 223 } 224 else if (!isQuotedString(value)) { 225 checkToken(value); 226 } 227 } 228 229 private boolean isQuotedString(String s) { 230 if (s.length() < 2) { 231 return false; 232 } 233 else { 234 return ((s.startsWith("\"") && s.endsWith("\"")) || (s.startsWith("'") && s.endsWith("'"))); 235 } 236 } 237 238 protected String unquote(String s) { 239 return (isQuotedString(s) ? s.substring(1, s.length() - 1) : s); 240 } 241 242 /** 243 * Indicates whether the {@linkplain #getType() type} is the wildcard character 244 * <code>*</code> or not. 245 */ 246 public boolean isWildcardType() { 247 return WILDCARD_TYPE.equals(getType()); 248 } 249 250 /** 251 * Indicates whether the {@linkplain #getSubtype() subtype} is the wildcard 252 * character <code>*</code> or the wildcard character followed by a suffix 253 * (e.g. <code>*+xml</code>). 254 * @return whether the subtype is a wildcard 255 */ 256 public boolean isWildcardSubtype() { 257 return WILDCARD_TYPE.equals(getSubtype()) || getSubtype().startsWith("*+"); 258 } 259 260 /** 261 * Indicates whether this MIME Type is concrete, i.e. whether neither the type 262 * nor the subtype is a wildcard character <code>*</code>. 263 * @return whether this MIME Type is concrete 264 */ 265 public boolean isConcrete() { 266 return !isWildcardType() && !isWildcardSubtype(); 267 } 268 269 /** 270 * Return the primary type. 271 */ 272 public String getType() { 273 return this.type; 274 } 275 276 /** 277 * Return the subtype. 278 */ 279 public String getSubtype() { 280 return this.subtype; 281 } 282 283 /** 284 * Return the character set, as indicated by a {@code charset} parameter, if any. 285 * @return the character set, or {@code null} if not available 286 * @since 4.3 287 */ 288 @Nullable 289 public Charset getCharset() { 290 return this.resolvedCharset; 291 } 292 293 /** 294 * Return a generic parameter value, given a parameter name. 295 * @param name the parameter name 296 * @return the parameter value, or {@code null} if not present 297 */ 298 @Nullable 299 public String getParameter(String name) { 300 return this.parameters.get(name); 301 } 302 303 /** 304 * Return all generic parameter values. 305 * @return a read-only map (possibly empty, never {@code null}) 306 */ 307 public Map<String, String> getParameters() { 308 return this.parameters; 309 } 310 311 /** 312 * Indicate whether this MIME Type includes the given MIME Type. 313 * <p>For instance, {@code text/*} includes {@code text/plain} and {@code text/html}, 314 * and {@code application/*+xml} includes {@code application/soap+xml}, etc. 315 * This method is <b>not</b> symmetric. 316 * @param other the reference MIME Type with which to compare 317 * @return {@code true} if this MIME Type includes the given MIME Type; 318 * {@code false} otherwise 319 */ 320 public boolean includes(@Nullable MimeType other) { 321 if (other == null) { 322 return false; 323 } 324 if (isWildcardType()) { 325 // */* includes anything 326 return true; 327 } 328 else if (getType().equals(other.getType())) { 329 if (getSubtype().equals(other.getSubtype())) { 330 return true; 331 } 332 if (isWildcardSubtype()) { 333 // Wildcard with suffix, e.g. application/*+xml 334 int thisPlusIdx = getSubtype().lastIndexOf('+'); 335 if (thisPlusIdx == -1) { 336 return true; 337 } 338 else { 339 // application/*+xml includes application/soap+xml 340 int otherPlusIdx = other.getSubtype().lastIndexOf('+'); 341 if (otherPlusIdx != -1) { 342 String thisSubtypeNoSuffix = getSubtype().substring(0, thisPlusIdx); 343 String thisSubtypeSuffix = getSubtype().substring(thisPlusIdx + 1); 344 String otherSubtypeSuffix = other.getSubtype().substring(otherPlusIdx + 1); 345 if (thisSubtypeSuffix.equals(otherSubtypeSuffix) && WILDCARD_TYPE.equals(thisSubtypeNoSuffix)) { 346 return true; 347 } 348 } 349 } 350 } 351 } 352 return false; 353 } 354 355 /** 356 * Indicate whether this MIME Type is compatible with the given MIME Type. 357 * <p>For instance, {@code text/*} is compatible with {@code text/plain}, 358 * {@code text/html}, and vice versa. In effect, this method is similar to 359 * {@link #includes}, except that it <b>is</b> symmetric. 360 * @param other the reference MIME Type with which to compare 361 * @return {@code true} if this MIME Type is compatible with the given MIME Type; 362 * {@code false} otherwise 363 */ 364 public boolean isCompatibleWith(@Nullable MimeType other) { 365 if (other == null) { 366 return false; 367 } 368 if (isWildcardType() || other.isWildcardType()) { 369 return true; 370 } 371 else if (getType().equals(other.getType())) { 372 if (getSubtype().equals(other.getSubtype())) { 373 return true; 374 } 375 // Wildcard with suffix? e.g. application/*+xml 376 if (isWildcardSubtype() || other.isWildcardSubtype()) { 377 int thisPlusIdx = getSubtype().lastIndexOf('+'); 378 int otherPlusIdx = other.getSubtype().lastIndexOf('+'); 379 if (thisPlusIdx == -1 && otherPlusIdx == -1) { 380 return true; 381 } 382 else if (thisPlusIdx != -1 && otherPlusIdx != -1) { 383 String thisSubtypeNoSuffix = getSubtype().substring(0, thisPlusIdx); 384 String otherSubtypeNoSuffix = other.getSubtype().substring(0, otherPlusIdx); 385 String thisSubtypeSuffix = getSubtype().substring(thisPlusIdx + 1); 386 String otherSubtypeSuffix = other.getSubtype().substring(otherPlusIdx + 1); 387 if (thisSubtypeSuffix.equals(otherSubtypeSuffix) && 388 (WILDCARD_TYPE.equals(thisSubtypeNoSuffix) || WILDCARD_TYPE.equals(otherSubtypeNoSuffix))) { 389 return true; 390 } 391 } 392 } 393 } 394 return false; 395 } 396 397 /** 398 * Similar to {@link #equals(Object)} but based on the type and subtype 399 * only, i.e. ignoring parameters. 400 * @param other the other mime type to compare to 401 * @return whether the two mime types have the same type and subtype 402 * @since 5.1.4 403 */ 404 public boolean equalsTypeAndSubtype(@Nullable MimeType other) { 405 if (other == null) { 406 return false; 407 } 408 return this.type.equalsIgnoreCase(other.type) && this.subtype.equalsIgnoreCase(other.subtype); 409 } 410 411 /** 412 * Unlike {@link Collection#contains(Object)} which relies on 413 * {@link MimeType#equals(Object)}, this method only checks the type and the 414 * subtype, but otherwise ignores parameters. 415 * @param mimeTypes the list of mime types to perform the check against 416 * @return whether the list contains the given mime type 417 * @since 5.1.4 418 */ 419 public boolean isPresentIn(Collection<? extends MimeType> mimeTypes) { 420 for (MimeType mimeType : mimeTypes) { 421 if (mimeType.equalsTypeAndSubtype(this)) { 422 return true; 423 } 424 } 425 return false; 426 } 427 428 429 @Override 430 public boolean equals(@Nullable Object other) { 431 if (this == other) { 432 return true; 433 } 434 if (!(other instanceof MimeType)) { 435 return false; 436 } 437 MimeType otherType = (MimeType) other; 438 return (this.type.equalsIgnoreCase(otherType.type) && 439 this.subtype.equalsIgnoreCase(otherType.subtype) && 440 parametersAreEqual(otherType)); 441 } 442 443 /** 444 * Determine if the parameters in this {@code MimeType} and the supplied 445 * {@code MimeType} are equal, performing case-insensitive comparisons 446 * for {@link Charset Charsets}. 447 * @since 4.2 448 */ 449 private boolean parametersAreEqual(MimeType other) { 450 if (this.parameters.size() != other.parameters.size()) { 451 return false; 452 } 453 454 for (Map.Entry<String, String> entry : this.parameters.entrySet()) { 455 String key = entry.getKey(); 456 if (!other.parameters.containsKey(key)) { 457 return false; 458 } 459 if (PARAM_CHARSET.equals(key)) { 460 if (!ObjectUtils.nullSafeEquals(getCharset(), other.getCharset())) { 461 return false; 462 } 463 } 464 else if (!ObjectUtils.nullSafeEquals(entry.getValue(), other.parameters.get(key))) { 465 return false; 466 } 467 } 468 469 return true; 470 } 471 472 @Override 473 public int hashCode() { 474 int result = this.type.hashCode(); 475 result = 31 * result + this.subtype.hashCode(); 476 result = 31 * result + this.parameters.hashCode(); 477 return result; 478 } 479 480 @Override 481 public String toString() { 482 String value = this.toStringValue; 483 if (value == null) { 484 StringBuilder builder = new StringBuilder(); 485 appendTo(builder); 486 value = builder.toString(); 487 this.toStringValue = value; 488 } 489 return value; 490 } 491 492 protected void appendTo(StringBuilder builder) { 493 builder.append(this.type); 494 builder.append('/'); 495 builder.append(this.subtype); 496 appendTo(this.parameters, builder); 497 } 498 499 private void appendTo(Map<String, String> map, StringBuilder builder) { 500 map.forEach((key, val) -> { 501 builder.append(';'); 502 builder.append(key); 503 builder.append('='); 504 builder.append(val); 505 }); 506 } 507 508 /** 509 * Compares this MIME Type to another alphabetically. 510 * @param other the MIME Type to compare to 511 * @see MimeTypeUtils#sortBySpecificity(List) 512 */ 513 @Override 514 public int compareTo(MimeType other) { 515 int comp = getType().compareToIgnoreCase(other.getType()); 516 if (comp != 0) { 517 return comp; 518 } 519 comp = getSubtype().compareToIgnoreCase(other.getSubtype()); 520 if (comp != 0) { 521 return comp; 522 } 523 comp = getParameters().size() - other.getParameters().size(); 524 if (comp != 0) { 525 return comp; 526 } 527 528 TreeSet<String> thisAttributes = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); 529 thisAttributes.addAll(getParameters().keySet()); 530 TreeSet<String> otherAttributes = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); 531 otherAttributes.addAll(other.getParameters().keySet()); 532 Iterator<String> thisAttributesIterator = thisAttributes.iterator(); 533 Iterator<String> otherAttributesIterator = otherAttributes.iterator(); 534 535 while (thisAttributesIterator.hasNext()) { 536 String thisAttribute = thisAttributesIterator.next(); 537 String otherAttribute = otherAttributesIterator.next(); 538 comp = thisAttribute.compareToIgnoreCase(otherAttribute); 539 if (comp != 0) { 540 return comp; 541 } 542 if (PARAM_CHARSET.equals(thisAttribute)) { 543 Charset thisCharset = getCharset(); 544 Charset otherCharset = other.getCharset(); 545 if (thisCharset != otherCharset) { 546 if (thisCharset == null) { 547 return -1; 548 } 549 if (otherCharset == null) { 550 return 1; 551 } 552 comp = thisCharset.compareTo(otherCharset); 553 if (comp != 0) { 554 return comp; 555 } 556 } 557 } 558 else { 559 String thisValue = getParameters().get(thisAttribute); 560 String otherValue = other.getParameters().get(otherAttribute); 561 if (otherValue == null) { 562 otherValue = ""; 563 } 564 comp = thisValue.compareTo(otherValue); 565 if (comp != 0) { 566 return comp; 567 } 568 } 569 } 570 571 return 0; 572 } 573 574 private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { 575 // Rely on default serialization, just initialize state after deserialization. 576 ois.defaultReadObject(); 577 578 // Initialize transient fields. 579 String charsetName = getParameter(PARAM_CHARSET); 580 if (charsetName != null) { 581 this.resolvedCharset = Charset.forName(unquote(charsetName)); 582 } 583 } 584 585 586 /** 587 * Parse the given String value into a {@code MimeType} object, 588 * with this method name following the 'valueOf' naming convention 589 * (as supported by {@link org.springframework.core.convert.ConversionService}. 590 * @see MimeTypeUtils#parseMimeType(String) 591 */ 592 public static MimeType valueOf(String value) { 593 return MimeTypeUtils.parseMimeType(value); 594 } 595 596 private static Map<String, String> addCharsetParameter(Charset charset, Map<String, String> parameters) { 597 Map<String, String> map = new LinkedHashMap<>(parameters); 598 map.put(PARAM_CHARSET, charset.name()); 599 return map; 600 } 601 602 603 /** 604 * Comparator to sort {@link MimeType MimeTypes} in order of specificity. 605 * 606 * @param <T> the type of mime types that may be compared by this comparator 607 */ 608 public static class SpecificityComparator<T extends MimeType> implements Comparator<T> { 609 610 @Override 611 public int compare(T mimeType1, T mimeType2) { 612 if (mimeType1.isWildcardType() && !mimeType2.isWildcardType()) { // */* < audio/* 613 return 1; 614 } 615 else if (mimeType2.isWildcardType() && !mimeType1.isWildcardType()) { // audio/* > */* 616 return -1; 617 } 618 else if (!mimeType1.getType().equals(mimeType2.getType())) { // audio/basic == text/html 619 return 0; 620 } 621 else { // mediaType1.getType().equals(mediaType2.getType()) 622 if (mimeType1.isWildcardSubtype() && !mimeType2.isWildcardSubtype()) { // audio/* < audio/basic 623 return 1; 624 } 625 else if (mimeType2.isWildcardSubtype() && !mimeType1.isWildcardSubtype()) { // audio/basic > audio/* 626 return -1; 627 } 628 else if (!mimeType1.getSubtype().equals(mimeType2.getSubtype())) { // audio/basic == audio/wave 629 return 0; 630 } 631 else { // mediaType2.getSubtype().equals(mediaType2.getSubtype()) 632 return compareParameters(mimeType1, mimeType2); 633 } 634 } 635 } 636 637 protected int compareParameters(T mimeType1, T mimeType2) { 638 int paramsSize1 = mimeType1.getParameters().size(); 639 int paramsSize2 = mimeType2.getParameters().size(); 640 return Integer.compare(paramsSize2, paramsSize1); // audio/basic;level=1 < audio/basic 641 } 642 } 643 644}