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. "&#42;/&#42;").
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 &lt; audio/* &lt; *&#047;*</blockquote>
365         * <blockquote>audio/basic;level=1 &lt; 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}