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.Charset; 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; 031 032import org.springframework.util.MimeType.SpecificityComparator; 033 034/** 035 * Miscellaneous {@link MimeType} utility methods. 036 * 037 * @author Arjen Poutsma 038 * @author Rossen Stoyanchev 039 * @since 4.0 040 */ 041@SuppressWarnings("deprecation") 042public abstract class MimeTypeUtils { 043 044 private static final byte[] BOUNDARY_CHARS = 045 new byte[] {'-', '_', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 046 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 047 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 048 'V', 'W', 'X', 'Y', 'Z'}; 049 050 private static final Random RND = new SecureRandom(); 051 052 private static Charset US_ASCII = Charset.forName("US-ASCII"); 053 054 /** 055 * Comparator used by {@link #sortBySpecificity(List)}. 056 */ 057 public static final Comparator<MimeType> SPECIFICITY_COMPARATOR = new SpecificityComparator<MimeType>(); 058 059 /** 060 * Public constant mime type that includes all media ranges (i.e. "*/*"). 061 */ 062 public static final MimeType ALL; 063 064 /** 065 * A String equivalent of {@link MimeTypeUtils#ALL}. 066 */ 067 public static final String ALL_VALUE = "*/*"; 068 069 /** 070 * Public constant mime type for {@code application/atom+xml}. 071 * @deprecated as of 4.3.6, in favor of {@code MediaType} constants 072 */ 073 @Deprecated 074 public static final MimeType APPLICATION_ATOM_XML; 075 076 /** 077 * A String equivalent of {@link MimeTypeUtils#APPLICATION_ATOM_XML}. 078 * @deprecated as of 4.3.6, in favor of {@code MediaType} constants 079 */ 080 @Deprecated 081 public static final String APPLICATION_ATOM_XML_VALUE = "application/atom+xml"; 082 083 /** 084 * Public constant mime type for {@code application/x-www-form-urlencoded}. 085 * @deprecated as of 4.3.6, in favor of {@code MediaType} constants 086 * */ 087 @Deprecated 088 public static final MimeType APPLICATION_FORM_URLENCODED; 089 090 /** 091 * A String equivalent of {@link MimeTypeUtils#APPLICATION_FORM_URLENCODED}. 092 * @deprecated as of 4.3.6, in favor of {@code MediaType} constants 093 */ 094 @Deprecated 095 public static final String APPLICATION_FORM_URLENCODED_VALUE = "application/x-www-form-urlencoded"; 096 097 /** 098 * Public constant mime type for {@code application/json}. 099 * */ 100 public static final MimeType APPLICATION_JSON; 101 102 /** 103 * A String equivalent of {@link MimeTypeUtils#APPLICATION_JSON}. 104 */ 105 public static final String APPLICATION_JSON_VALUE = "application/json"; 106 107 /** 108 * Public constant mime type for {@code application/octet-stream}. 109 * */ 110 public static final MimeType APPLICATION_OCTET_STREAM; 111 112 /** 113 * A String equivalent of {@link MimeTypeUtils#APPLICATION_OCTET_STREAM}. 114 */ 115 public static final String APPLICATION_OCTET_STREAM_VALUE = "application/octet-stream"; 116 117 /** 118 * Public constant mime type for {@code application/xhtml+xml}. 119 * @deprecated as of 4.3.6, in favor of {@code MediaType} constants 120 */ 121 @Deprecated 122 public static final MimeType APPLICATION_XHTML_XML; 123 124 /** 125 * A String equivalent of {@link MimeTypeUtils#APPLICATION_XHTML_XML}. 126 * @deprecated as of 4.3.6, in favor of {@code MediaType} constants 127 */ 128 @Deprecated 129 public static final String APPLICATION_XHTML_XML_VALUE = "application/xhtml+xml"; 130 131 /** 132 * Public constant mime type for {@code application/xml}. 133 */ 134 public static final MimeType APPLICATION_XML; 135 136 /** 137 * A String equivalent of {@link MimeTypeUtils#APPLICATION_XML}. 138 */ 139 public static final String APPLICATION_XML_VALUE = "application/xml"; 140 141 /** 142 * Public constant mime type for {@code image/gif}. 143 */ 144 public static final MimeType IMAGE_GIF; 145 146 /** 147 * A String equivalent of {@link MimeTypeUtils#IMAGE_GIF}. 148 */ 149 public static final String IMAGE_GIF_VALUE = "image/gif"; 150 151 /** 152 * Public constant mime type for {@code image/jpeg}. 153 */ 154 public static final MimeType IMAGE_JPEG; 155 156 /** 157 * A String equivalent of {@link MimeTypeUtils#IMAGE_JPEG}. 158 */ 159 public static final String IMAGE_JPEG_VALUE = "image/jpeg"; 160 161 /** 162 * Public constant mime type for {@code image/png}. 163 */ 164 public static final MimeType IMAGE_PNG; 165 166 /** 167 * A String equivalent of {@link MimeTypeUtils#IMAGE_PNG}. 168 */ 169 public static final String IMAGE_PNG_VALUE = "image/png"; 170 171 /** 172 * Public constant mime type for {@code multipart/form-data}. 173 * @deprecated as of 4.3.6, in favor of {@code MediaType} constants 174 */ 175 @Deprecated 176 public static final MimeType MULTIPART_FORM_DATA; 177 178 /** 179 * A String equivalent of {@link MimeTypeUtils#MULTIPART_FORM_DATA}. 180 * @deprecated as of 4.3.6, in favor of {@code MediaType} constants 181 */ 182 @Deprecated 183 public static final String MULTIPART_FORM_DATA_VALUE = "multipart/form-data"; 184 185 /** 186 * Public constant mime type for {@code text/html}. 187 * */ 188 public static final MimeType TEXT_HTML; 189 190 /** 191 * A String equivalent of {@link MimeTypeUtils#TEXT_HTML}. 192 */ 193 public static final String TEXT_HTML_VALUE = "text/html"; 194 195 /** 196 * Public constant mime type for {@code text/plain}. 197 * */ 198 public static final MimeType TEXT_PLAIN; 199 200 /** 201 * A String equivalent of {@link MimeTypeUtils#TEXT_PLAIN}. 202 */ 203 public static final String TEXT_PLAIN_VALUE = "text/plain"; 204 205 /** 206 * Public constant mime type for {@code text/xml}. 207 * */ 208 public static final MimeType TEXT_XML; 209 210 /** 211 * A String equivalent of {@link MimeTypeUtils#TEXT_XML}. 212 */ 213 public static final String TEXT_XML_VALUE = "text/xml"; 214 215 216 static { 217 ALL = MimeType.valueOf(ALL_VALUE); 218 APPLICATION_ATOM_XML = MimeType.valueOf(APPLICATION_ATOM_XML_VALUE); 219 APPLICATION_FORM_URLENCODED = MimeType.valueOf(APPLICATION_FORM_URLENCODED_VALUE); 220 APPLICATION_JSON = MimeType.valueOf(APPLICATION_JSON_VALUE); 221 APPLICATION_OCTET_STREAM = MimeType.valueOf(APPLICATION_OCTET_STREAM_VALUE); 222 APPLICATION_XHTML_XML = MimeType.valueOf(APPLICATION_XHTML_XML_VALUE); 223 APPLICATION_XML = MimeType.valueOf(APPLICATION_XML_VALUE); 224 IMAGE_GIF = MimeType.valueOf(IMAGE_GIF_VALUE); 225 IMAGE_JPEG = MimeType.valueOf(IMAGE_JPEG_VALUE); 226 IMAGE_PNG = MimeType.valueOf(IMAGE_PNG_VALUE); 227 MULTIPART_FORM_DATA = MimeType.valueOf(MULTIPART_FORM_DATA_VALUE); 228 TEXT_HTML = MimeType.valueOf(TEXT_HTML_VALUE); 229 TEXT_PLAIN = MimeType.valueOf(TEXT_PLAIN_VALUE); 230 TEXT_XML = MimeType.valueOf(TEXT_XML_VALUE); 231 } 232 233 234 /** 235 * Parse the given String into a single {@code MimeType}. 236 * @param mimeType the string to parse 237 * @return the mime type 238 * @throws InvalidMimeTypeException if the string cannot be parsed 239 */ 240 public static MimeType parseMimeType(String mimeType) { 241 if (!StringUtils.hasLength(mimeType)) { 242 throw new InvalidMimeTypeException(mimeType, "'mimeType' must not be empty"); 243 } 244 245 int index = mimeType.indexOf(';'); 246 String fullType = (index >= 0 ? mimeType.substring(0, index) : mimeType).trim(); 247 if (fullType.isEmpty()) { 248 throw new InvalidMimeTypeException(mimeType, "'mimeType' must not be empty"); 249 } 250 251 // java.net.HttpURLConnection returns a *; q=.2 Accept header 252 if (MimeType.WILDCARD_TYPE.equals(fullType)) { 253 fullType = "*/*"; 254 } 255 int subIndex = fullType.indexOf('/'); 256 if (subIndex == -1) { 257 throw new InvalidMimeTypeException(mimeType, "does not contain '/'"); 258 } 259 if (subIndex == fullType.length() - 1) { 260 throw new InvalidMimeTypeException(mimeType, "does not contain subtype after '/'"); 261 } 262 String type = fullType.substring(0, subIndex); 263 String subtype = fullType.substring(subIndex + 1); 264 if (MimeType.WILDCARD_TYPE.equals(type) && !MimeType.WILDCARD_TYPE.equals(subtype)) { 265 throw new InvalidMimeTypeException(mimeType, "wildcard type is legal only in '*/*' (all mime types)"); 266 } 267 268 Map<String, String> parameters = null; 269 do { 270 int nextIndex = index + 1; 271 boolean quoted = false; 272 while (nextIndex < mimeType.length()) { 273 char ch = mimeType.charAt(nextIndex); 274 if (ch == ';') { 275 if (!quoted) { 276 break; 277 } 278 } 279 else if (ch == '"') { 280 quoted = !quoted; 281 } 282 nextIndex++; 283 } 284 String parameter = mimeType.substring(index + 1, nextIndex).trim(); 285 if (parameter.length() > 0) { 286 if (parameters == null) { 287 parameters = new LinkedHashMap<String, String>(4); 288 } 289 int eqIndex = parameter.indexOf('='); 290 if (eqIndex >= 0) { 291 String attribute = parameter.substring(0, eqIndex).trim(); 292 String value = parameter.substring(eqIndex + 1, parameter.length()).trim(); 293 parameters.put(attribute, value); 294 } 295 } 296 index = nextIndex; 297 } 298 while (index < mimeType.length()); 299 300 try { 301 return new MimeType(type, subtype, parameters); 302 } 303 catch (UnsupportedCharsetException ex) { 304 throw new InvalidMimeTypeException(mimeType, "unsupported charset '" + ex.getCharsetName() + "'"); 305 } 306 catch (IllegalArgumentException ex) { 307 throw new InvalidMimeTypeException(mimeType, ex.getMessage()); 308 } 309 } 310 311 /** 312 * Parse the given, comma-separated string into a list of {@code MimeType} objects. 313 * @param mimeTypes the string to parse 314 * @return the list of mime types 315 * @throws IllegalArgumentException if the string cannot be parsed 316 */ 317 public static List<MimeType> parseMimeTypes(String mimeTypes) { 318 if (!StringUtils.hasLength(mimeTypes)) { 319 return Collections.emptyList(); 320 } 321 String[] tokens = StringUtils.tokenizeToStringArray(mimeTypes, ","); 322 List<MimeType> result = new ArrayList<MimeType>(tokens.length); 323 for (String token : tokens) { 324 result.add(parseMimeType(token)); 325 } 326 return result; 327 } 328 329 /** 330 * Return a string representation of the given list of {@code MimeType} objects. 331 * @param mimeTypes the string to parse 332 * @return the list of mime types 333 * @throws IllegalArgumentException if the String cannot be parsed 334 */ 335 public static String toString(Collection<? extends MimeType> mimeTypes) { 336 StringBuilder builder = new StringBuilder(); 337 for (Iterator<? extends MimeType> iterator = mimeTypes.iterator(); iterator.hasNext();) { 338 MimeType mimeType = iterator.next(); 339 mimeType.appendTo(builder); 340 if (iterator.hasNext()) { 341 builder.append(", "); 342 } 343 } 344 return builder.toString(); 345 } 346 347 348 /** 349 * Sorts the given list of {@code MimeType} objects by specificity. 350 * <p>Given two mime types: 351 * <ol> 352 * <li>if either mime type has a {@linkplain MimeType#isWildcardType() wildcard type}, 353 * then the mime type without the wildcard is ordered before the other.</li> 354 * <li>if the two mime types have different {@linkplain MimeType#getType() types}, 355 * then they are considered equal and remain their current order.</li> 356 * <li>if either mime type has a {@linkplain MimeType#isWildcardSubtype() wildcard subtype} 357 * , then the mime type without the wildcard is sorted before the other.</li> 358 * <li>if the two mime types have different {@linkplain MimeType#getSubtype() subtypes}, 359 * then they are considered equal and remain their current order.</li> 360 * <li>if the two mime types have a different amount of 361 * {@linkplain MimeType#getParameter(String) parameters}, then the mime type with the most 362 * parameters is ordered before the other.</li> 363 * </ol> 364 * <p>For example: <blockquote>audio/basic < audio/* < */*</blockquote> 365 * <blockquote>audio/basic;level=1 < audio/basic</blockquote> 366 * <blockquote>audio/basic == text/html</blockquote> <blockquote>audio/basic == 367 * audio/wave</blockquote> 368 * @param mimeTypes the list of mime types to be sorted 369 * @see <a href="https://tools.ietf.org/html/rfc7231#section-5.3.2">HTTP 1.1: Semantics 370 * and Content, section 5.3.2</a> 371 */ 372 public static void sortBySpecificity(List<MimeType> mimeTypes) { 373 Assert.notNull(mimeTypes, "'mimeTypes' must not be null"); 374 if (mimeTypes.size() > 1) { 375 Collections.sort(mimeTypes, SPECIFICITY_COMPARATOR); 376 } 377 } 378 379 /** 380 * Generate a random MIME boundary as bytes, often used in multipart mime types. 381 */ 382 public static byte[] generateMultipartBoundary() { 383 byte[] boundary = new byte[RND.nextInt(11) + 30]; 384 for (int i = 0; i < boundary.length; i++) { 385 boundary[i] = BOUNDARY_CHARS[RND.nextInt(BOUNDARY_CHARS.length)]; 386 } 387 return boundary; 388 } 389 390 /** 391 * Generate a random MIME boundary as String, often used in multipart mime types. 392 */ 393 public static String generateMultipartBoundaryString() { 394 return new String(generateMultipartBoundary(), US_ASCII); 395 } 396 397}