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