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.nio.charset.StandardCharsets; 020import java.nio.charset.UnsupportedCharsetException; 021import java.security.SecureRandom; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.Collections; 025import java.util.Comparator; 026import java.util.Iterator; 027import java.util.LinkedHashMap; 028import java.util.List; 029import java.util.Map; 030import java.util.Random; 031import java.util.concurrent.ConcurrentHashMap; 032import java.util.concurrent.ConcurrentLinkedDeque; 033import java.util.concurrent.locks.ReadWriteLock; 034import java.util.concurrent.locks.ReentrantReadWriteLock; 035import java.util.function.Function; 036import java.util.stream.Collectors; 037 038import org.springframework.lang.Nullable; 039 040/** 041 * Miscellaneous {@link MimeType} utility methods. 042 * 043 * @author Arjen Poutsma 044 * @author Rossen Stoyanchev 045 * @author Dimitrios Liapis 046 * @author Brian Clozel 047 * @author Sam Brannen 048 * @since 4.0 049 */ 050public abstract class MimeTypeUtils { 051 052 private static final byte[] BOUNDARY_CHARS = 053 new byte[] {'-', '_', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 054 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 055 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 056 'V', 'W', 'X', 'Y', 'Z'}; 057 058 /** 059 * Comparator used by {@link #sortBySpecificity(List)}. 060 */ 061 public static final Comparator<MimeType> SPECIFICITY_COMPARATOR = new MimeType.SpecificityComparator<>(); 062 063 /** 064 * Public constant mime type that includes all media ranges (i.e. "*/*"). 065 */ 066 public static final MimeType ALL; 067 068 /** 069 * A String equivalent of {@link MimeTypeUtils#ALL}. 070 */ 071 public static final String ALL_VALUE = "*/*"; 072 073 /** 074 * Public constant mime type for {@code application/json}. 075 * */ 076 public static final MimeType APPLICATION_JSON; 077 078 /** 079 * A String equivalent of {@link MimeTypeUtils#APPLICATION_JSON}. 080 */ 081 public static final String APPLICATION_JSON_VALUE = "application/json"; 082 083 /** 084 * Public constant mime type for {@code application/octet-stream}. 085 * */ 086 public static final MimeType APPLICATION_OCTET_STREAM; 087 088 /** 089 * A String equivalent of {@link MimeTypeUtils#APPLICATION_OCTET_STREAM}. 090 */ 091 public static final String APPLICATION_OCTET_STREAM_VALUE = "application/octet-stream"; 092 093 /** 094 * Public constant mime type for {@code application/xml}. 095 */ 096 public static final MimeType APPLICATION_XML; 097 098 /** 099 * A String equivalent of {@link MimeTypeUtils#APPLICATION_XML}. 100 */ 101 public static final String APPLICATION_XML_VALUE = "application/xml"; 102 103 /** 104 * Public constant mime type for {@code image/gif}. 105 */ 106 public static final MimeType IMAGE_GIF; 107 108 /** 109 * A String equivalent of {@link MimeTypeUtils#IMAGE_GIF}. 110 */ 111 public static final String IMAGE_GIF_VALUE = "image/gif"; 112 113 /** 114 * Public constant mime type for {@code image/jpeg}. 115 */ 116 public static final MimeType IMAGE_JPEG; 117 118 /** 119 * A String equivalent of {@link MimeTypeUtils#IMAGE_JPEG}. 120 */ 121 public static final String IMAGE_JPEG_VALUE = "image/jpeg"; 122 123 /** 124 * Public constant mime type for {@code image/png}. 125 */ 126 public static final MimeType IMAGE_PNG; 127 128 /** 129 * A String equivalent of {@link MimeTypeUtils#IMAGE_PNG}. 130 */ 131 public static final String IMAGE_PNG_VALUE = "image/png"; 132 133 /** 134 * Public constant mime type for {@code text/html}. 135 * */ 136 public static final MimeType TEXT_HTML; 137 138 /** 139 * A String equivalent of {@link MimeTypeUtils#TEXT_HTML}. 140 */ 141 public static final String TEXT_HTML_VALUE = "text/html"; 142 143 /** 144 * Public constant mime type for {@code text/plain}. 145 * */ 146 public static final MimeType TEXT_PLAIN; 147 148 /** 149 * A String equivalent of {@link MimeTypeUtils#TEXT_PLAIN}. 150 */ 151 public static final String TEXT_PLAIN_VALUE = "text/plain"; 152 153 /** 154 * Public constant mime type for {@code text/xml}. 155 * */ 156 public static final MimeType TEXT_XML; 157 158 /** 159 * A String equivalent of {@link MimeTypeUtils#TEXT_XML}. 160 */ 161 public static final String TEXT_XML_VALUE = "text/xml"; 162 163 164 private static final ConcurrentLruCache<String, MimeType> cachedMimeTypes = 165 new ConcurrentLruCache<>(64, MimeTypeUtils::parseMimeTypeInternal); 166 167 @Nullable 168 private static volatile Random random; 169 170 static { 171 // Not using "parseMimeType" to avoid static init cost 172 ALL = new MimeType("*", "*"); 173 APPLICATION_JSON = new MimeType("application", "json"); 174 APPLICATION_OCTET_STREAM = new MimeType("application", "octet-stream"); 175 APPLICATION_XML = new MimeType("application", "xml"); 176 IMAGE_GIF = new MimeType("image", "gif"); 177 IMAGE_JPEG = new MimeType("image", "jpeg"); 178 IMAGE_PNG = new MimeType("image", "png"); 179 TEXT_HTML = new MimeType("text", "html"); 180 TEXT_PLAIN = new MimeType("text", "plain"); 181 TEXT_XML = new MimeType("text", "xml"); 182 } 183 184 185 /** 186 * Parse the given String into a single {@code MimeType}. 187 * Recently parsed {@code MimeType} are cached for further retrieval. 188 * @param mimeType the string to parse 189 * @return the mime type 190 * @throws InvalidMimeTypeException if the string cannot be parsed 191 */ 192 public static MimeType parseMimeType(String mimeType) { 193 if (!StringUtils.hasLength(mimeType)) { 194 throw new InvalidMimeTypeException(mimeType, "'mimeType' must not be empty"); 195 } 196 // do not cache multipart mime types with random boundaries 197 if (mimeType.startsWith("multipart")) { 198 return parseMimeTypeInternal(mimeType); 199 } 200 return cachedMimeTypes.get(mimeType); 201 } 202 203 private static MimeType parseMimeTypeInternal(String mimeType) { 204 int index = mimeType.indexOf(';'); 205 String fullType = (index >= 0 ? mimeType.substring(0, index) : mimeType).trim(); 206 if (fullType.isEmpty()) { 207 throw new InvalidMimeTypeException(mimeType, "'mimeType' must not be empty"); 208 } 209 210 // java.net.HttpURLConnection returns a *; q=.2 Accept header 211 if (MimeType.WILDCARD_TYPE.equals(fullType)) { 212 fullType = "*/*"; 213 } 214 int subIndex = fullType.indexOf('/'); 215 if (subIndex == -1) { 216 throw new InvalidMimeTypeException(mimeType, "does not contain '/'"); 217 } 218 if (subIndex == fullType.length() - 1) { 219 throw new InvalidMimeTypeException(mimeType, "does not contain subtype after '/'"); 220 } 221 String type = fullType.substring(0, subIndex); 222 String subtype = fullType.substring(subIndex + 1); 223 if (MimeType.WILDCARD_TYPE.equals(type) && !MimeType.WILDCARD_TYPE.equals(subtype)) { 224 throw new InvalidMimeTypeException(mimeType, "wildcard type is legal only in '*/*' (all mime types)"); 225 } 226 227 Map<String, String> parameters = null; 228 do { 229 int nextIndex = index + 1; 230 boolean quoted = false; 231 while (nextIndex < mimeType.length()) { 232 char ch = mimeType.charAt(nextIndex); 233 if (ch == ';') { 234 if (!quoted) { 235 break; 236 } 237 } 238 else if (ch == '"') { 239 quoted = !quoted; 240 } 241 nextIndex++; 242 } 243 String parameter = mimeType.substring(index + 1, nextIndex).trim(); 244 if (parameter.length() > 0) { 245 if (parameters == null) { 246 parameters = new LinkedHashMap<>(4); 247 } 248 int eqIndex = parameter.indexOf('='); 249 if (eqIndex >= 0) { 250 String attribute = parameter.substring(0, eqIndex).trim(); 251 String value = parameter.substring(eqIndex + 1).trim(); 252 parameters.put(attribute, value); 253 } 254 } 255 index = nextIndex; 256 } 257 while (index < mimeType.length()); 258 259 try { 260 return new MimeType(type, subtype, parameters); 261 } 262 catch (UnsupportedCharsetException ex) { 263 throw new InvalidMimeTypeException(mimeType, "unsupported charset '" + ex.getCharsetName() + "'"); 264 } 265 catch (IllegalArgumentException ex) { 266 throw new InvalidMimeTypeException(mimeType, ex.getMessage()); 267 } 268 } 269 270 /** 271 * Parse the comma-separated string into a list of {@code MimeType} objects. 272 * @param mimeTypes the string to parse 273 * @return the list of mime types 274 * @throws InvalidMimeTypeException if the string cannot be parsed 275 */ 276 public static List<MimeType> parseMimeTypes(String mimeTypes) { 277 if (!StringUtils.hasLength(mimeTypes)) { 278 return Collections.emptyList(); 279 } 280 return tokenize(mimeTypes).stream() 281 .filter(StringUtils::hasText) 282 .map(MimeTypeUtils::parseMimeType) 283 .collect(Collectors.toList()); 284 } 285 286 /** 287 * Tokenize the given comma-separated string of {@code MimeType} objects 288 * into a {@code List<String>}. Unlike simple tokenization by ",", this 289 * method takes into account quoted parameters. 290 * @param mimeTypes the string to tokenize 291 * @return the list of tokens 292 * @since 5.1.3 293 */ 294 public static List<String> tokenize(String mimeTypes) { 295 if (!StringUtils.hasLength(mimeTypes)) { 296 return Collections.emptyList(); 297 } 298 List<String> tokens = new ArrayList<>(); 299 boolean inQuotes = false; 300 int startIndex = 0; 301 int i = 0; 302 while (i < mimeTypes.length()) { 303 switch (mimeTypes.charAt(i)) { 304 case '"': 305 inQuotes = !inQuotes; 306 break; 307 case ',': 308 if (!inQuotes) { 309 tokens.add(mimeTypes.substring(startIndex, i)); 310 startIndex = i + 1; 311 } 312 break; 313 case '\\': 314 i++; 315 break; 316 } 317 i++; 318 } 319 tokens.add(mimeTypes.substring(startIndex)); 320 return tokens; 321 } 322 323 /** 324 * Return a string representation of the given list of {@code MimeType} objects. 325 * @param mimeTypes the string to parse 326 * @return the list of mime types 327 * @throws IllegalArgumentException if the String cannot be parsed 328 */ 329 public static String toString(Collection<? extends MimeType> mimeTypes) { 330 StringBuilder builder = new StringBuilder(); 331 for (Iterator<? extends MimeType> iterator = mimeTypes.iterator(); iterator.hasNext();) { 332 MimeType mimeType = iterator.next(); 333 mimeType.appendTo(builder); 334 if (iterator.hasNext()) { 335 builder.append(", "); 336 } 337 } 338 return builder.toString(); 339 } 340 341 /** 342 * Sorts the given list of {@code MimeType} objects by specificity. 343 * <p>Given two mime types: 344 * <ol> 345 * <li>if either mime type has a {@linkplain MimeType#isWildcardType() wildcard type}, 346 * then the mime type without the wildcard is ordered before the other.</li> 347 * <li>if the two mime types have different {@linkplain MimeType#getType() types}, 348 * then they are considered equal and remain their current order.</li> 349 * <li>if either mime type has a {@linkplain MimeType#isWildcardSubtype() wildcard subtype} 350 * , then the mime type without the wildcard is sorted before the other.</li> 351 * <li>if the two mime types have different {@linkplain MimeType#getSubtype() subtypes}, 352 * then they are considered equal and remain their current order.</li> 353 * <li>if the two mime types have a different amount of 354 * {@linkplain MimeType#getParameter(String) parameters}, then the mime type with the most 355 * parameters is ordered before the other.</li> 356 * </ol> 357 * <p>For example: <blockquote>audio/basic < audio/* < */*</blockquote> 358 * <blockquote>audio/basic;level=1 < audio/basic</blockquote> 359 * <blockquote>audio/basic == text/html</blockquote> <blockquote>audio/basic == 360 * audio/wave</blockquote> 361 * @param mimeTypes the list of mime types to be sorted 362 * @see <a href="https://tools.ietf.org/html/rfc7231#section-5.3.2">HTTP 1.1: Semantics 363 * and Content, section 5.3.2</a> 364 */ 365 public static void sortBySpecificity(List<MimeType> mimeTypes) { 366 Assert.notNull(mimeTypes, "'mimeTypes' must not be null"); 367 if (mimeTypes.size() > 1) { 368 mimeTypes.sort(SPECIFICITY_COMPARATOR); 369 } 370 } 371 372 373 /** 374 * Lazily initialize the {@link SecureRandom} for {@link #generateMultipartBoundary()}. 375 */ 376 private static Random initRandom() { 377 Random randomToUse = random; 378 if (randomToUse == null) { 379 synchronized (MimeTypeUtils.class) { 380 randomToUse = random; 381 if (randomToUse == null) { 382 randomToUse = new SecureRandom(); 383 random = randomToUse; 384 } 385 } 386 } 387 return randomToUse; 388 } 389 390 /** 391 * Generate a random MIME boundary as bytes, often used in multipart mime types. 392 */ 393 public static byte[] generateMultipartBoundary() { 394 Random randomToUse = initRandom(); 395 byte[] boundary = new byte[randomToUse.nextInt(11) + 30]; 396 for (int i = 0; i < boundary.length; i++) { 397 boundary[i] = BOUNDARY_CHARS[randomToUse.nextInt(BOUNDARY_CHARS.length)]; 398 } 399 return boundary; 400 } 401 402 /** 403 * Generate a random MIME boundary as String, often used in multipart mime types. 404 */ 405 public static String generateMultipartBoundaryString() { 406 return new String(generateMultipartBoundary(), StandardCharsets.US_ASCII); 407 } 408 409 410 /** 411 * Simple Least Recently Used cache, bounded by the maximum size given 412 * to the class constructor. 413 * <p>This implementation is backed by a {@code ConcurrentHashMap} for storing 414 * the cached values and a {@code ConcurrentLinkedQueue} for ordering the keys 415 * and choosing the least recently used key when the cache is at full capacity. 416 * @param <K> the type of the key used for caching 417 * @param <V> the type of the cached values 418 */ 419 private static class ConcurrentLruCache<K, V> { 420 421 private final int maxSize; 422 423 private final ConcurrentLinkedDeque<K> queue = new ConcurrentLinkedDeque<>(); 424 425 private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>(); 426 427 private final ReadWriteLock lock; 428 429 private final Function<K, V> generator; 430 431 private volatile int size; 432 433 public ConcurrentLruCache(int maxSize, Function<K, V> generator) { 434 Assert.isTrue(maxSize > 0, "LRU max size should be positive"); 435 Assert.notNull(generator, "Generator function should not be null"); 436 this.maxSize = maxSize; 437 this.generator = generator; 438 this.lock = new ReentrantReadWriteLock(); 439 } 440 441 public V get(K key) { 442 V cached = this.cache.get(key); 443 if (cached != null) { 444 if (this.size < this.maxSize) { 445 return cached; 446 } 447 this.lock.readLock().lock(); 448 try { 449 if (this.queue.removeLastOccurrence(key)) { 450 this.queue.offer(key); 451 } 452 return cached; 453 } 454 finally { 455 this.lock.readLock().unlock(); 456 } 457 } 458 this.lock.writeLock().lock(); 459 try { 460 // Retrying in case of concurrent reads on the same key 461 cached = this.cache.get(key); 462 if (cached != null) { 463 if (this.queue.removeLastOccurrence(key)) { 464 this.queue.offer(key); 465 } 466 return cached; 467 } 468 // Generate value first, to prevent size inconsistency 469 V value = this.generator.apply(key); 470 int cacheSize = this.size; 471 if (cacheSize == this.maxSize) { 472 K leastUsed = this.queue.poll(); 473 if (leastUsed != null) { 474 this.cache.remove(leastUsed); 475 cacheSize--; 476 } 477 } 478 this.queue.offer(key); 479 this.cache.put(key, value); 480 this.size = cacheSize + 1; 481 return value; 482 } 483 finally { 484 this.lock.writeLock().unlock(); 485 } 486 } 487 } 488 489}