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>"&#42;"</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>&#42;</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>&#42;</code> or the wildcard character followed by a suffix
243         * (e.g. <code>&#42;+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>&#42;</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}