All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.util.MimeType Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2002-2015 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.util;

import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.BitSet;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeSet;

/**
 * Represents a MIME Type, as originally defined in RFC 2046 and subsequently used in
 * other Internet protocols including HTTP.
 *
 * 

This class, however, does not contain support for the q-parameters used * in HTTP content negotiation. Those can be found in the sub-class * {@code org.springframework.http.MediaType} in the {@code spring-web} module. * *

Consists of a {@linkplain #getType() type} and a {@linkplain #getSubtype() subtype}. * Also has functionality to parse media types from a string using * {@link #valueOf(String)}. For more parsing options see {@link MimeTypeUtils}. * * @author Arjen Poutsma * @author Juergen Hoeller * @author Rossen Stoyanchev * @author Sam Brannen * @since 4.0 * @see MimeTypeUtils */ public class MimeType implements Comparable, Serializable { private static final long serialVersionUID = 4085923477777865903L; protected static final String WILDCARD_TYPE = "*"; private static final BitSet TOKEN; private static final String PARAM_CHARSET = "charset"; private final String type; private final String subtype; private final Map parameters; static { // variable names refer to RFC 2616, section 2.2 BitSet ctl = new BitSet(128); for (int i = 0; i <= 31; i++) { ctl.set(i); } ctl.set(127); BitSet separators = new BitSet(128); separators.set('('); separators.set(')'); separators.set('<'); separators.set('>'); separators.set('@'); separators.set(','); separators.set(';'); separators.set(':'); separators.set('\\'); separators.set('\"'); separators.set('/'); separators.set('['); separators.set(']'); separators.set('?'); separators.set('='); separators.set('{'); separators.set('}'); separators.set(' '); separators.set('\t'); TOKEN = new BitSet(128); TOKEN.set(0, 128); TOKEN.andNot(ctl); TOKEN.andNot(separators); } /** * Create a new {@code MimeType} for the given primary type. *

The {@linkplain #getSubtype() subtype} is set to "*", * and the parameters are empty. * @param type the primary type * @throws IllegalArgumentException if any of the parameters contains illegal characters */ public MimeType(String type) { this(type, WILDCARD_TYPE); } /** * Create a new {@code MimeType} for the given primary type and subtype. *

The parameters are empty. * @param type the primary type * @param subtype the subtype * @throws IllegalArgumentException if any of the parameters contains illegal characters */ public MimeType(String type, String subtype) { this(type, subtype, Collections.emptyMap()); } /** * Create a new {@code MimeType} for the given type, subtype, and character set. * @param type the primary type * @param subtype the subtype * @param charSet the character set * @throws IllegalArgumentException if any of the parameters contains illegal characters */ public MimeType(String type, String subtype, Charset charSet) { this(type, subtype, Collections.singletonMap(PARAM_CHARSET, charSet.name())); } /** * Copy-constructor that copies the type and subtype of the given {@code MimeType}, * and allows for different parameter. * @param other the other media type * @param parameters the parameters, may be {@code null} * @throws IllegalArgumentException if any of the parameters contains illegal characters */ public MimeType(MimeType other, Map parameters) { this(other.getType(), other.getSubtype(), parameters); } /** * Create a new {@code MimeType} for the given type, subtype, and parameters. * @param type the primary type * @param subtype the subtype * @param parameters the parameters, may be {@code null} * @throws IllegalArgumentException if any of the parameters contains illegal characters */ public MimeType(String type, String subtype, Map parameters) { Assert.hasLength(type, "type must not be empty"); Assert.hasLength(subtype, "subtype must not be empty"); checkToken(type); checkToken(subtype); this.type = type.toLowerCase(Locale.ENGLISH); this.subtype = subtype.toLowerCase(Locale.ENGLISH); if (!CollectionUtils.isEmpty(parameters)) { Map map = new LinkedCaseInsensitiveMap(parameters.size(), Locale.ENGLISH); for (Map.Entry entry : parameters.entrySet()) { String attribute = entry.getKey(); String value = entry.getValue(); checkParameters(attribute, value); map.put(attribute, value); } this.parameters = Collections.unmodifiableMap(map); } else { this.parameters = Collections.emptyMap(); } } /** * Checks the given token string for illegal characters, as defined in RFC 2616, * section 2.2. * @throws IllegalArgumentException in case of illegal characters * @see HTTP 1.1, section 2.2 */ private void checkToken(String token) { for (int i=0; i < token.length(); i++ ) { char ch = token.charAt(i); if (!TOKEN.get(ch)) { throw new IllegalArgumentException("Invalid token character '" + ch + "' in token \"" + token + "\""); } } } protected void checkParameters(String attribute, String value) { Assert.hasLength(attribute, "parameter attribute must not be empty"); Assert.hasLength(value, "parameter value must not be empty"); checkToken(attribute); if (PARAM_CHARSET.equals(attribute)) { value = unquote(value); Charset.forName(value); } else if (!isQuotedString(value)) { checkToken(value); } } private boolean isQuotedString(String s) { if (s.length() < 2) { return false; } else { return ((s.startsWith("\"") && s.endsWith("\"")) || (s.startsWith("'") && s.endsWith("'"))); } } protected String unquote(String s) { if (s == null) { return null; } return isQuotedString(s) ? s.substring(1, s.length() - 1) : s; } /** * Indicates whether the {@linkplain #getType() type} is the wildcard character * * or not. */ public boolean isWildcardType() { return WILDCARD_TYPE.equals(getType()); } /** * Indicates whether the {@linkplain #getSubtype() subtype} is the wildcard * character * or the wildcard character followed by a suffix * (e.g. *+xml). * @return whether the subtype is a wildcard */ public boolean isWildcardSubtype() { return WILDCARD_TYPE.equals(getSubtype()) || getSubtype().startsWith("*+"); } /** * Indicates whether this media type is concrete, i.e. whether neither the type * nor the subtype is a wildcard character *. * @return whether this media type is concrete */ public boolean isConcrete() { return !isWildcardType() && !isWildcardSubtype(); } /** * Return the primary type. */ public String getType() { return this.type; } /** * Return the subtype. */ public String getSubtype() { return this.subtype; } /** * Return the character set, as indicated by a {@code charset} parameter, if any. * @return the character set, or {@code null} if not available */ public Charset getCharSet() { String charSet = getParameter(PARAM_CHARSET); return (charSet != null ? Charset.forName(unquote(charSet)) : null); } /** * Return a generic parameter value, given a parameter name. * @param name the parameter name * @return the parameter value, or {@code null} if not present */ public String getParameter(String name) { return this.parameters.get(name); } /** * Return all generic parameter values. * @return a read-only map (possibly empty, never {@code null}) */ public Map getParameters() { return this.parameters; } /** * Indicate whether this {@code MediaType} includes the given media type. *

For instance, {@code text/*} includes {@code text/plain} and {@code text/html}, * and {@code application/*+xml} includes {@code application/soap+xml}, etc. This * method is not symmetric. * @param other the reference media type with which to compare * @return {@code true} if this media type includes the given media type; * {@code false} otherwise */ public boolean includes(MimeType other) { if (other == null) { return false; } if (this.isWildcardType()) { // */* includes anything return true; } else if (getType().equals(other.getType())) { if (getSubtype().equals(other.getSubtype())) { return true; } if (this.isWildcardSubtype()) { // wildcard with suffix, e.g. application/*+xml int thisPlusIdx = getSubtype().indexOf('+'); if (thisPlusIdx == -1) { return true; } else { // application/*+xml includes application/soap+xml int otherPlusIdx = other.getSubtype().indexOf('+'); if (otherPlusIdx != -1) { String thisSubtypeNoSuffix = getSubtype().substring(0, thisPlusIdx); String thisSubtypeSuffix = getSubtype().substring(thisPlusIdx + 1); String otherSubtypeSuffix = other.getSubtype().substring(otherPlusIdx + 1); if (thisSubtypeSuffix.equals(otherSubtypeSuffix) && WILDCARD_TYPE.equals(thisSubtypeNoSuffix)) { return true; } } } } } return false; } /** * Indicate whether this {@code MediaType} is compatible with the given media type. *

For instance, {@code text/*} is compatible with {@code text/plain}, * {@code text/html}, and vice versa. In effect, this method is similar to * {@link #includes}, except that it is symmetric. * @param other the reference media type with which to compare * @return {@code true} if this media type is compatible with the given media type; * {@code false} otherwise */ public boolean isCompatibleWith(MimeType other) { if (other == null) { return false; } if (isWildcardType() || other.isWildcardType()) { return true; } else if (getType().equals(other.getType())) { if (getSubtype().equals(other.getSubtype())) { return true; } // wildcard with suffix? e.g. application/*+xml if (this.isWildcardSubtype() || other.isWildcardSubtype()) { int thisPlusIdx = getSubtype().indexOf('+'); int otherPlusIdx = other.getSubtype().indexOf('+'); if (thisPlusIdx == -1 && otherPlusIdx == -1) { return true; } else if (thisPlusIdx != -1 && otherPlusIdx != -1) { String thisSubtypeNoSuffix = getSubtype().substring(0, thisPlusIdx); String otherSubtypeNoSuffix = other.getSubtype().substring(0, otherPlusIdx); String thisSubtypeSuffix = getSubtype().substring(thisPlusIdx + 1); String otherSubtypeSuffix = other.getSubtype().substring(otherPlusIdx + 1); if (thisSubtypeSuffix.equals(otherSubtypeSuffix) && (WILDCARD_TYPE.equals(thisSubtypeNoSuffix) || WILDCARD_TYPE.equals(otherSubtypeNoSuffix))) { return true; } } } } return false; } /** * Compares this {@code MediaType} to another alphabetically. * @param other media type to compare to * @see MimeTypeUtils#sortBySpecificity(List) */ @Override public int compareTo(MimeType other) { int comp = getType().compareToIgnoreCase(other.getType()); if (comp != 0) { return comp; } comp = getSubtype().compareToIgnoreCase(other.getSubtype()); if (comp != 0) { return comp; } comp = getParameters().size() - other.getParameters().size(); if (comp != 0) { return comp; } TreeSet thisAttributes = new TreeSet(String.CASE_INSENSITIVE_ORDER); thisAttributes.addAll(getParameters().keySet()); TreeSet otherAttributes = new TreeSet(String.CASE_INSENSITIVE_ORDER); otherAttributes.addAll(other.getParameters().keySet()); Iterator thisAttributesIterator = thisAttributes.iterator(); Iterator otherAttributesIterator = otherAttributes.iterator(); while (thisAttributesIterator.hasNext()) { String thisAttribute = thisAttributesIterator.next(); String otherAttribute = otherAttributesIterator.next(); comp = thisAttribute.compareToIgnoreCase(otherAttribute); if (comp != 0) { return comp; } String thisValue = getParameters().get(thisAttribute); String otherValue = other.getParameters().get(otherAttribute); if (otherValue == null) { otherValue = ""; } comp = thisValue.compareTo(otherValue); if (comp != 0) { return comp; } } return 0; } @Override public boolean equals(Object other) { if (this == other) { return true; } if (!(other instanceof MimeType)) { return false; } MimeType otherType = (MimeType) other; return (this.type.equalsIgnoreCase(otherType.type) && this.subtype.equalsIgnoreCase(otherType.subtype) && parametersAreEqual(otherType)); } /** * Determine if the parameters in this {@code MimeType} and the supplied * {@code MimeType} are equal, performing case-insensitive comparisons * for {@link Charset}s. * @since 4.2 */ private boolean parametersAreEqual(MimeType that) { if (this.parameters.size() != that.parameters.size()) { return false; } for (String key : this.parameters.keySet()) { if (!that.parameters.containsKey(key)) { return false; } if (PARAM_CHARSET.equals(key)) { if (!ObjectUtils.nullSafeEquals(this.getCharSet(), that.getCharSet())) { return false; } } else if (!ObjectUtils.nullSafeEquals(this.parameters.get(key), that.parameters.get(key))) { return false; } } return true; } @Override public int hashCode() { int result = this.type.hashCode(); result = 31 * result + this.subtype.hashCode(); result = 31 * result + this.parameters.hashCode(); return result; } @Override public String toString() { StringBuilder builder = new StringBuilder(); appendTo(builder); return builder.toString(); } protected void appendTo(StringBuilder builder) { builder.append(this.type); builder.append('/'); builder.append(this.subtype); appendTo(this.parameters, builder); } private void appendTo(Map map, StringBuilder builder) { for (Map.Entry entry : map.entrySet()) { builder.append(';'); builder.append(entry.getKey()); builder.append('='); builder.append(entry.getValue()); } } /** * Parse the given String value into a {@code MimeType} object, * with this method name following the 'valueOf' naming convention * (as supported by {@link org.springframework.core.convert.ConversionService}. * @see MimeTypeUtils#parseMimeType(String) */ public static MimeType valueOf(String value) { return MimeTypeUtils.parseMimeType(value); } public static class SpecificityComparator implements Comparator { @Override public int compare(T mimeType1, T mimeType2) { if (mimeType1.isWildcardType() && !mimeType2.isWildcardType()) { // */* < audio/* return 1; } else if (mimeType2.isWildcardType() && !mimeType1.isWildcardType()) { // audio/* > */* return -1; } else if (!mimeType1.getType().equals(mimeType2.getType())) { // audio/basic == text/html return 0; } else { // mediaType1.getType().equals(mediaType2.getType()) if (mimeType1.isWildcardSubtype() && !mimeType2.isWildcardSubtype()) { // audio/* < audio/basic return 1; } else if (mimeType2.isWildcardSubtype() && !mimeType1.isWildcardSubtype()) { // audio/basic > audio/* return -1; } else if (!mimeType1.getSubtype().equals(mimeType2.getSubtype())) { // audio/basic == audio/wave return 0; } else { // mediaType2.getSubtype().equals(mediaType2.getSubtype()) return compareParameters(mimeType1, mimeType2); } } } protected int compareParameters(T mimeType1, T mimeType2) { int paramsSize1 = mimeType1.getParameters().size(); int paramsSize2 = mimeType2.getParameters().size(); return (paramsSize2 < paramsSize1 ? -1 : (paramsSize2 == paramsSize1 ? 0 : 1)); // audio/basic;level=1 < audio/basic } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy