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.Serializable; 020import java.nio.charset.Charset; 021import java.util.BitSet; 022import java.util.Collections; 023import java.util.Comparator; 024import java.util.Iterator; 025import java.util.LinkedHashMap; 026import java.util.List; 027import java.util.Locale; 028import java.util.Map; 029import java.util.TreeSet; 030 031/** 032 * Represents a MIME Type, as originally defined in RFC 2046 and subsequently 033 * used in other Internet protocols including HTTP. 034 * 035 * <p>This class, however, does not contain support for the q-parameters used 036 * in HTTP content negotiation. Those can be found in the subclass 037 * {@code org.springframework.http.MediaType} in the {@code spring-web} module. 038 * 039 * <p>Consists of a {@linkplain #getType() type} and a {@linkplain #getSubtype() subtype}. 040 * Also has functionality to parse MIME Type values from a {@code String} using 041 * {@link #valueOf(String)}. For more parsing options see {@link MimeTypeUtils}. 042 * 043 * @author Arjen Poutsma 044 * @author Juergen Hoeller 045 * @author Rossen Stoyanchev 046 * @author Sam Brannen 047 * @since 4.0 048 * @see MimeTypeUtils 049 */ 050public class MimeType implements Comparable<MimeType>, Serializable { 051 052 private static final long serialVersionUID = 4085923477777865903L; 053 054 055 protected static final String WILDCARD_TYPE = "*"; 056 057 private static final String PARAM_CHARSET = "charset"; 058 059 private static final BitSet TOKEN; 060 061 static { 062 // variable names refer to RFC 2616, section 2.2 063 BitSet ctl = new BitSet(128); 064 for (int i = 0; i <= 31; i++) { 065 ctl.set(i); 066 } 067 ctl.set(127); 068 069 BitSet separators = new BitSet(128); 070 separators.set('('); 071 separators.set(')'); 072 separators.set('<'); 073 separators.set('>'); 074 separators.set('@'); 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('\t'); 089 090 TOKEN = new BitSet(128); 091 TOKEN.set(0, 128); 092 TOKEN.andNot(ctl); 093 TOKEN.andNot(separators); 094 } 095 096 097 private final String type; 098 099 private final String subtype; 100 101 private final Map<String, String> parameters; 102 103 104 /** 105 * Create a new {@code MimeType} for the given primary type. 106 * <p>The {@linkplain #getSubtype() subtype} is set to <code>"*"</code>, 107 * and the parameters are empty. 108 * @param type the primary type 109 * @throws IllegalArgumentException if any of the parameters contains illegal characters 110 */ 111 public MimeType(String type) { 112 this(type, WILDCARD_TYPE); 113 } 114 115 /** 116 * Create a new {@code MimeType} for the given primary type and subtype. 117 * <p>The parameters are empty. 118 * @param type the primary type 119 * @param subtype the subtype 120 * @throws IllegalArgumentException if any of the parameters contains illegal characters 121 */ 122 public MimeType(String type, String subtype) { 123 this(type, subtype, Collections.<String, String>emptyMap()); 124 } 125 126 /** 127 * Create a new {@code MimeType} for the given type, subtype, and character set. 128 * @param type the primary type 129 * @param subtype the subtype 130 * @param charset the character set 131 * @throws IllegalArgumentException if any of the parameters contains illegal characters 132 */ 133 public MimeType(String type, String subtype, Charset charset) { 134 this(type, subtype, Collections.singletonMap(PARAM_CHARSET, charset.name())); 135 } 136 137 /** 138 * Copy-constructor that copies the type, subtype, parameters of the given {@code MimeType}, 139 * and allows to set the specified character set. 140 * @param other the other MimeType 141 * @param charset the character set 142 * @throws IllegalArgumentException if any of the parameters contains illegal characters 143 * @since 4.3 144 */ 145 public MimeType(MimeType other, Charset charset) { 146 this(other.getType(), other.getSubtype(), addCharsetParameter(charset, other.getParameters())); 147 } 148 149 /** 150 * Copy-constructor that copies the type and subtype of the given {@code MimeType}, 151 * and allows for different parameter. 152 * @param other the other MimeType 153 * @param parameters the parameters (may be {@code null}) 154 * @throws IllegalArgumentException if any of the parameters contains illegal characters 155 */ 156 public MimeType(MimeType other, Map<String, String> parameters) { 157 this(other.getType(), other.getSubtype(), parameters); 158 } 159 160 /** 161 * Create a new {@code MimeType} for the given type, subtype, and parameters. 162 * @param type the primary type 163 * @param subtype the subtype 164 * @param parameters the parameters (may be {@code null}) 165 * @throws IllegalArgumentException if any of the parameters contains illegal characters 166 */ 167 public MimeType(String type, String subtype, Map<String, String> parameters) { 168 Assert.hasLength(type, "'type' must not be empty"); 169 Assert.hasLength(subtype, "'subtype' must not be empty"); 170 checkToken(type); 171 checkToken(subtype); 172 this.type = type.toLowerCase(Locale.ENGLISH); 173 this.subtype = subtype.toLowerCase(Locale.ENGLISH); 174 if (!CollectionUtils.isEmpty(parameters)) { 175 Map<String, String> map = new LinkedCaseInsensitiveMap<String>(parameters.size(), Locale.ENGLISH); 176 for (Map.Entry<String, String> entry : parameters.entrySet()) { 177 String attribute = entry.getKey(); 178 String value = entry.getValue(); 179 checkParameters(attribute, value); 180 map.put(attribute, value); 181 } 182 this.parameters = Collections.unmodifiableMap(map); 183 } 184 else { 185 this.parameters = Collections.emptyMap(); 186 } 187 } 188 189 /** 190 * Checks the given token string for illegal characters, as defined in RFC 2616, 191 * section 2.2. 192 * @throws IllegalArgumentException in case of illegal characters 193 * @see <a href="https://tools.ietf.org/html/rfc2616#section-2.2">HTTP 1.1, section 2.2</a> 194 */ 195 private void checkToken(String token) { 196 for (int i = 0; i < token.length(); i++) { 197 char ch = token.charAt(i); 198 if (!TOKEN.get(ch)) { 199 throw new IllegalArgumentException("Invalid token character '" + ch + "' in token \"" + token + "\""); 200 } 201 } 202 } 203 204 protected void checkParameters(String attribute, String value) { 205 Assert.hasLength(attribute, "'attribute' must not be empty"); 206 Assert.hasLength(value, "'value' must not be empty"); 207 checkToken(attribute); 208 if (PARAM_CHARSET.equals(attribute)) { 209 Charset.forName(unquote(value)); 210 } 211 else if (!isQuotedString(value)) { 212 checkToken(value); 213 } 214 } 215 216 private boolean isQuotedString(String s) { 217 if (s.length() < 2) { 218 return false; 219 } 220 else { 221 return ((s.startsWith("\"") && s.endsWith("\"")) || (s.startsWith("'") && s.endsWith("'"))); 222 } 223 } 224 225 protected String unquote(String s) { 226 if (s == null) { 227 return null; 228 } 229 return (isQuotedString(s) ? s.substring(1, s.length() - 1) : s); 230 } 231 232 /** 233 * Indicates whether the {@linkplain #getType() type} is the wildcard character 234 * <code>*</code> or not. 235 */ 236 public boolean isWildcardType() { 237 return WILDCARD_TYPE.equals(getType()); 238 } 239 240 /** 241 * Indicates whether the {@linkplain #getSubtype() subtype} is the wildcard 242 * character <code>*</code> or the wildcard character followed by a suffix 243 * (e.g. <code>*+xml</code>). 244 * @return whether the subtype is a wildcard 245 */ 246 public boolean isWildcardSubtype() { 247 return WILDCARD_TYPE.equals(getSubtype()) || getSubtype().startsWith("*+"); 248 } 249 250 /** 251 * Indicates whether this MIME Type is concrete, i.e. whether neither the type 252 * nor the subtype is a wildcard character <code>*</code>. 253 * @return whether this MIME Type is concrete 254 */ 255 public boolean isConcrete() { 256 return !isWildcardType() && !isWildcardSubtype(); 257 } 258 259 /** 260 * Return the primary type. 261 */ 262 public String getType() { 263 return this.type; 264 } 265 266 /** 267 * Return the subtype. 268 */ 269 public String getSubtype() { 270 return this.subtype; 271 } 272 273 /** 274 * Return the character set, as indicated by a {@code charset} parameter, if any. 275 * @return the character set, or {@code null} if not available 276 * @since 4.3 277 */ 278 public Charset getCharset() { 279 String charset = getParameter(PARAM_CHARSET); 280 return (charset != null ? Charset.forName(unquote(charset)) : null); 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 * @deprecated as of Spring 4.3, in favor of {@link #getCharset()} with its name 287 * aligned with the Java return type name 288 */ 289 @Deprecated 290 public Charset getCharSet() { 291 return getCharset(); 292 } 293 294 /** 295 * Return a generic parameter value, given a parameter name. 296 * @param name the parameter name 297 * @return the parameter value, or {@code null} if not present 298 */ 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(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().indexOf('+'); 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(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().indexOf('+'); 378 int otherPlusIdx = other.getSubtype().indexOf('+'); 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 @Override 399 public boolean equals(Object other) { 400 if (this == other) { 401 return true; 402 } 403 if (!(other instanceof MimeType)) { 404 return false; 405 } 406 MimeType otherType = (MimeType) other; 407 return (this.type.equalsIgnoreCase(otherType.type) && 408 this.subtype.equalsIgnoreCase(otherType.subtype) && 409 parametersAreEqual(otherType)); 410 } 411 412 /** 413 * Determine if the parameters in this {@code MimeType} and the supplied 414 * {@code MimeType} are equal, performing case-insensitive comparisons 415 * for {@link Charset}s. 416 * @since 4.2 417 */ 418 private boolean parametersAreEqual(MimeType other) { 419 if (this.parameters.size() != other.parameters.size()) { 420 return false; 421 } 422 423 for (String key : this.parameters.keySet()) { 424 if (!other.parameters.containsKey(key)) { 425 return false; 426 } 427 if (PARAM_CHARSET.equals(key)) { 428 if (!ObjectUtils.nullSafeEquals(getCharset(), other.getCharset())) { 429 return false; 430 } 431 } 432 else if (!ObjectUtils.nullSafeEquals(this.parameters.get(key), other.parameters.get(key))) { 433 return false; 434 } 435 } 436 437 return true; 438 } 439 440 @Override 441 public int hashCode() { 442 int result = this.type.hashCode(); 443 result = 31 * result + this.subtype.hashCode(); 444 result = 31 * result + this.parameters.hashCode(); 445 return result; 446 } 447 448 @Override 449 public String toString() { 450 StringBuilder builder = new StringBuilder(); 451 appendTo(builder); 452 return builder.toString(); 453 } 454 455 protected void appendTo(StringBuilder builder) { 456 builder.append(this.type); 457 builder.append('/'); 458 builder.append(this.subtype); 459 appendTo(this.parameters, builder); 460 } 461 462 private void appendTo(Map<String, String> map, StringBuilder builder) { 463 for (Map.Entry<String, String> entry : map.entrySet()) { 464 builder.append(';'); 465 builder.append(entry.getKey()); 466 builder.append('='); 467 builder.append(entry.getValue()); 468 } 469 } 470 471 /** 472 * Compares this MIME Type to another alphabetically. 473 * @param other the MIME Type to compare to 474 * @see MimeTypeUtils#sortBySpecificity(List) 475 */ 476 @Override 477 public int compareTo(MimeType other) { 478 int comp = getType().compareToIgnoreCase(other.getType()); 479 if (comp != 0) { 480 return comp; 481 } 482 comp = getSubtype().compareToIgnoreCase(other.getSubtype()); 483 if (comp != 0) { 484 return comp; 485 } 486 comp = getParameters().size() - other.getParameters().size(); 487 if (comp != 0) { 488 return comp; 489 } 490 491 TreeSet<String> thisAttributes = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER); 492 thisAttributes.addAll(getParameters().keySet()); 493 TreeSet<String> otherAttributes = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER); 494 otherAttributes.addAll(other.getParameters().keySet()); 495 Iterator<String> thisAttributesIterator = thisAttributes.iterator(); 496 Iterator<String> otherAttributesIterator = otherAttributes.iterator(); 497 498 while (thisAttributesIterator.hasNext()) { 499 String thisAttribute = thisAttributesIterator.next(); 500 String otherAttribute = otherAttributesIterator.next(); 501 comp = thisAttribute.compareToIgnoreCase(otherAttribute); 502 if (comp != 0) { 503 return comp; 504 } 505 if (PARAM_CHARSET.equals(thisAttribute)) { 506 Charset thisCharset = getCharset(); 507 Charset otherCharset = other.getCharset(); 508 if (thisCharset != otherCharset) { 509 if (thisCharset == null) { 510 return -1; 511 } 512 if (otherCharset == null) { 513 return 1; 514 } 515 comp = thisCharset.compareTo(otherCharset); 516 if (comp != 0) { 517 return comp; 518 } 519 } 520 } 521 else { 522 String thisValue = getParameters().get(thisAttribute); 523 String otherValue = other.getParameters().get(otherAttribute); 524 if (otherValue == null) { 525 otherValue = ""; 526 } 527 comp = thisValue.compareTo(otherValue); 528 if (comp != 0) { 529 return comp; 530 } 531 } 532 } 533 534 return 0; 535 } 536 537 538 /** 539 * Parse the given String value into a {@code MimeType} object, 540 * with this method name following the 'valueOf' naming convention 541 * (as supported by {@link org.springframework.core.convert.ConversionService}. 542 * @see MimeTypeUtils#parseMimeType(String) 543 */ 544 public static MimeType valueOf(String value) { 545 return MimeTypeUtils.parseMimeType(value); 546 } 547 548 private static Map<String, String> addCharsetParameter(Charset charset, Map<String, String> parameters) { 549 Map<String, String> map = new LinkedHashMap<String, String>(parameters); 550 map.put(PARAM_CHARSET, charset.name()); 551 return map; 552 } 553 554 555 public static class SpecificityComparator<T extends MimeType> implements Comparator<T> { 556 557 @Override 558 public int compare(T mimeType1, T mimeType2) { 559 if (mimeType1.isWildcardType() && !mimeType2.isWildcardType()) { // */* < audio/* 560 return 1; 561 } 562 else if (mimeType2.isWildcardType() && !mimeType1.isWildcardType()) { // audio/* > */* 563 return -1; 564 } 565 else if (!mimeType1.getType().equals(mimeType2.getType())) { // audio/basic == text/html 566 return 0; 567 } 568 else { // mediaType1.getType().equals(mediaType2.getType()) 569 if (mimeType1.isWildcardSubtype() && !mimeType2.isWildcardSubtype()) { // audio/* < audio/basic 570 return 1; 571 } 572 else if (mimeType2.isWildcardSubtype() && !mimeType1.isWildcardSubtype()) { // audio/basic > audio/* 573 return -1; 574 } 575 else if (!mimeType1.getSubtype().equals(mimeType2.getSubtype())) { // audio/basic == audio/wave 576 return 0; 577 } 578 else { // mediaType2.getSubtype().equals(mediaType2.getSubtype()) 579 return compareParameters(mimeType1, mimeType2); 580 } 581 } 582 } 583 584 protected int compareParameters(T mimeType1, T mimeType2) { 585 int paramsSize1 = mimeType1.getParameters().size(); 586 int paramsSize2 = mimeType2.getParameters().size(); 587 return (paramsSize2 < paramsSize1 ? -1 : (paramsSize2 == paramsSize1 ? 0 : 1)); // audio/basic;level=1 < audio/basic 588 } 589 } 590 591}