/* * Copyright (C) 2014 Stefan Niederhauser (nidin@gmx.ch) * * 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 guru.nidi.ramltester.util; import java.util.*; /** * */ public final class MediaType { public static final Comparator<MediaType> QUALITY_COMPARATOR = new Comparator<MediaType>() { @Override public int compare(MediaType m1, MediaType m2) { return Double.compare(m2.getQualityParameter(), m1.getQualityParameter()); } }; public static final MediaType JSON = valueOf("application/json"), FORM_URL_ENCODED = valueOf("application/x-www-form-urlencoded"), MULTIPART = valueOf("multipart/form-data"); private static final String CHARSET = "charset"; private static final String WILDCARD_TYPE = "*"; private static final Map<String, MediaType> KNOWN_SUFFICES = new HashMap<>(); static { KNOWN_SUFFICES.put("json", JSON); } private final String type; private final String subtype; private final Map<String, String> parameters; private MediaType(String type, String subtype, Map<String, String> parameters) { this.type = type; this.subtype = subtype; this.parameters = parameters; } public static MediaType valueOf(String mimeType) { if (mimeType == null || mimeType.length() == 0) { throw new InvalidMediaTypeException(mimeType, new Message("mediaType.empty")); } final String[] parts = tokenizeToStringArray(mimeType, ";"); String fullType = parts[0].trim(); // java.net.HttpURLConnection returns a *; q=.2 Accept header if (WILDCARD_TYPE.equals(fullType)) { fullType = "*/*"; } final int subIndex = fullType.indexOf('/'); if (subIndex == -1) { throw new InvalidMediaTypeException(mimeType, new Message("mediaType.noSlash")); } if (subIndex == fullType.length() - 1) { throw new InvalidMediaTypeException(mimeType, new Message("mediaType.noSubtype")); } final String type = fullType.substring(0, subIndex); final String subtype = fullType.substring(subIndex + 1, fullType.length()); if (WILDCARD_TYPE.equals(type) && !WILDCARD_TYPE.equals(subtype)) { throw new InvalidMediaTypeException(mimeType, new Message("mediaType.wildcard.illegal")); } final Map<String, String> parameters = parseParameters(parts); for (final Map.Entry<String, String> entry : parameters.entrySet()) { checkParameter(mimeType, entry.getKey(), entry.getValue()); } return new MediaType(type, subtype, parameters); } private static Map<String, String> parseParameters(String[] parts) { final Map<String, String> parameters = new LinkedHashMap<>(parts.length); if (parts.length > 1) { for (int i = 1; i < parts.length; i++) { final String parameter = parts[i]; final int eqIndex = parameter.indexOf('='); if (eqIndex != -1) { final String attribute = parameter.substring(0, eqIndex); final String value = parameter.substring(eqIndex + 1, parameter.length()); parameters.put(attribute, value); } } } return parameters; } public double getQualityParameter() { final String q = unquote(getParameter("q")); return q == null ? 1 : Double.parseDouble(q); } private static void checkParameter(String mimeType, String key, String value) { if ("q".equals(key)) { final String unquoted = unquote(value); try { final double d = Double.parseDouble(unquoted); if (d < 0 || d > 1) { throw new InvalidMediaTypeException(mimeType, new Message("mediaType.quality.illegal", unquoted)); } } catch (NumberFormatException e) { throw new InvalidMediaTypeException(mimeType, new Message("mediaType.quality.illegal", unquoted)); } } } private static boolean isQuotedString(String s) { return s.length() >= 2 && ((s.startsWith("\"") && s.endsWith("\"")) || (s.startsWith("'") && s.endsWith("'"))); } private static String unquote(String s) { if (s == null) { return null; } return isQuotedString(s) ? s.substring(1, s.length() - 1) : s; } public boolean isWildcardType() { return WILDCARD_TYPE.equals(getType()); } public boolean isWildcardSubtype() { return WILDCARD_TYPE.equals(getSubtype()) || getSubtype().startsWith("*+"); } public int similarity(MediaType other) { int s = 0; final int init = 3 * 3 * 3 * 3; int factor = init; final Object[] myParts = parts(); final Object[] otherParts = other.parts(); int i = 0; for (; i < myParts.length; i++) { if (WILDCARD_TYPE.equals(myParts[i]) || (WILDCARD_TYPE.equals(otherParts[i]))) { s += factor; } else if (myParts[i].equals(otherParts[i])) { s += 2 * factor; } else { break; } factor /= 3; } //only first part matches -> not similar at all return i <= 1 ? 0 : s; } private String[] parts() { final String suffix = getSuffix(); if (suffix == null) { return new String[]{type, subtype, "*", "*", parameters.toString()}; } final MediaType known = KNOWN_SUFFICES.get(suffix); return known == null ? new String[]{"unknown", suffix, type, getPureSubtype(), parameters.toString()} : new String[]{known.type, known.subtype, type, getPureSubtype(), parameters.toString()}; } public boolean isCompatibleWith(MediaType other) { if (other == null) { return false; } if (isWildcardType() || other.isWildcardType()) { return true; } final MediaType thisKnown = applyKnownSuffices(); final MediaType otherKnown = other.applyKnownSuffices(); if ((thisKnown != this || otherKnown != other) && thisKnown.isCompatibleWith(otherKnown)) { return true; } if (!getType().equals(other.getType())) { return false; } if (getSubtype().equals(other.getSubtype())) { return true; } // wildcard with suffix? e.g. application/*+xml return isThisOrOtherWildcardSubtype(other) && hasSameSuffix(other); } private boolean isThisOrOtherWildcardSubtype(MediaType other) { return isWildcardSubtype() || other.isWildcardSubtype(); } private boolean hasSameSuffix(MediaType other) { final String thisSuffix = getSuffix(); final String otherSuffix = other.getSuffix(); return (thisSuffix == null && otherSuffix == null) || (thisSuffix != null && thisSuffix.equals(otherSuffix)); } private MediaType applyKnownSuffices() { final MediaType known = KNOWN_SUFFICES.get(getSuffix()); return known == null ? this : known; } public String getType() { return this.type; } public String getSubtype() { return subtype; } public String getPureSubtype() { final int pos = getSubtype().indexOf('+'); return pos == -1 ? getSubtype() : getSubtype().substring(0, pos); } public String getSuffix() { final int pos = getSubtype().indexOf('+'); return pos == -1 ? null : getSubtype().substring(pos + 1); } public Map<String, String> getParameters() { return Collections.unmodifiableMap(parameters); } public String getParameter(String name) { return parameters.get(name); } public String getCharset(String defaultCharset) { final String charset = parameters.get(CHARSET); return charset == null ? defaultCharset : charset; } private static String[] tokenizeToStringArray(String str, String delimiters) { if (str == null) { return null; } final StringTokenizer st = new StringTokenizer(str, delimiters); final List<String> tokens = new ArrayList<>(); while (st.hasMoreTokens()) { String token = st.nextToken(); token = token.trim(); if (token.length() > 0) { tokens.add(token); } } return toStringArray(tokens); } private static String[] toStringArray(Collection<String> collection) { return collection == null ? null : collection.toArray(new String[collection.size()]); } @Override public boolean equals(Object other) { if (this == other) { return true; } if (!(other instanceof MediaType)) { return false; } final MediaType otherType = (MediaType) other; return (this.type.equalsIgnoreCase(otherType.type) && this.subtype.equalsIgnoreCase(otherType.subtype) && this.parameters.equals(otherType.parameters)); } @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() { String res = type + "/" + subtype; for (final Map.Entry<String, String> entry : parameters.entrySet()) { res += ";" + entry.getKey() + "=" + entry.getValue(); } return res; } }