blob: 1d20e79cd5f618f137b3b0b19be876008a7a6fb7 [file] [log] [blame]
// ***************************************************************************************************************************
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
// * to you 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.apache.juneau.http;
import static org.apache.juneau.http.Constants.*;
import static org.apache.juneau.internal.StringUtils.*;
import static org.apache.juneau.internal.ObjectUtils.*;
import java.util.*;
import org.apache.http.*;
import org.apache.http.message.*;
import org.apache.juneau.annotation.*;
import org.apache.juneau.collections.*;
import org.apache.juneau.internal.*;
import org.apache.juneau.json.*;
/**
* Describes a single media type used in content negotiation between an HTTP client and server, as described in
* Section 14.1 and 14.7 of RFC2616 (the HTTP/1.1 specification).
*
* <ul class='seealso'>
* <li class='extlink'>{@doc ExtRFC2616}
* </ul>
*/
@BeanIgnore
public class MediaType implements Comparable<MediaType> {
private static final Cache<String,MediaType> CACHE = new Cache<>(NOCACHE, CACHE_MAX_SIZE);
/** Reusable predefined media type */
@SuppressWarnings("javadoc")
public static final MediaType
CSV = of("text/csv"),
HTML = of("text/html"),
JSON = of("application/json"),
MSGPACK = of("octal/msgpack"),
PLAIN = of("text/plain"),
UON = of("text/uon"),
URLENCODING = of("application/x-www-form-urlencoded"),
XML = of("text/xml"),
XMLSOAP = of("text/xml+soap"),
RDF = of("text/xml+rdf"),
RDFABBREV = of("text/xml+rdf+abbrev"),
NTRIPLE = of("text/n-triple"),
TURTLE = of("text/turtle"),
N3 = of("text/n3")
;
private final String string; // The entire unparsed value.
private final String mediaType; // The "type/subtype" portion of the media type..
private final String type; // The media type (e.g. "text" for Accept, "utf-8" for Accept-Charset)
private final String subType; // The media sub-type (e.g. "json" for Accept, not used for Accept-Charset)
private final String[] subTypes; // The media sub-type (e.g. "json" for Accept, not used for Accept-Charset)
private final String[] subTypesSorted; // Same as subTypes, but sorted so that it can be used for comparison.
private final boolean hasSubtypeMeta; // The media subtype contains meta-character '*'.
private final NameValuePair[] parameters; // The media type parameters (e.g. "text/html;level=1"). Does not include q!
/**
* Returns the media type for the specified string.
* The same media type strings always return the same objects so that these objects
* can be compared for equality using '=='.
*
* <ul class='notes'>
* <li>
* Spaces are replaced with <js>'+'</js> characters.
* This gets around the issue where passing media type strings with <js>'+'</js> as HTTP GET parameters
* get replaced with spaces by your browser. Since spaces aren't supported by the spec, this
* is doesn't break anything.
* <li>
* Anything including and following the <js>';'</js> character is ignored (e.g. <js>";charset=X"</js>).
* </ul>
*
* @param value
* The media type string.
* Will be lowercased.
* Returns <jk>null</jk> if input is null or empty.
* @return A cached media type object.
*/
public static MediaType of(String value) {
if (isEmpty(value))
return null;
MediaType x = CACHE.get(value);
if (x == null)
x = CACHE.put(value, new MediaType(value));
return x;
}
/**
* Same as {@link #of(String)} but allows you to construct an array of <c>MediaTypes</c> from an
* array of strings.
*
* @param values
* The media type strings.
* @return
* An array of <c>MediaType</c> objects.
* <br>Always the same length as the input string array.
*/
public static MediaType[] ofAll(String...values) {
MediaType[] mt = new MediaType[values.length];
for (int i = 0; i < values.length; i++)
mt[i] = of(values[i]);
return mt;
}
/**
* Constructor.
*
* @param mt The media type string.
*/
public MediaType(String mt) {
this(parse(mt));
}
/**
* Constructor.
*
* @param e The parsed media type string.
*/
public MediaType(HeaderElement e) {
mediaType = e.getName();
List<NameValuePair> parameters = AList.create();
for (NameValuePair p : e.getParameters()) {
if (p.getName().equals("q"))
break;
parameters.add(BasicNameValuePair.of(p.getName(), p.getValue()));
}
this.parameters= parameters.toArray(new NameValuePair[parameters.size()]);
String x = mediaType.replace(' ', '+');
int i = x.indexOf('/');
type = (i == -1 ? x : x.substring(0, i));
subType = (i == -1 ? "*" : x.substring(i+1));
subTypes = StringUtils.split(subType, '+');
subTypesSorted = Arrays.copyOf(subTypes, subTypes.length);
Arrays.sort(this.subTypesSorted);
hasSubtypeMeta = ArrayUtils.contains("*", this.subTypes);
StringBuilder sb = new StringBuilder();
sb.append(mediaType);
for (NameValuePair p : parameters)
sb.append(';').append(p.getName()).append('=').append(p.getValue());
this.string = sb.toString();
}
/**
* Returns the <js>'type'</js> fragment of the <js>'type/subType'</js> string.
*
* @return The media type.
*/
public final String getType() {
return type;
}
/**
* Returns the <js>'subType'</js> fragment of the <js>'type/subType'</js> string.
*
* @return The media subtype.
*/
public final String getSubType() {
return subType;
}
/**
* Returns <jk>true</jk> if the subtype contains the specified <js>'+'</js> delimited subtype value.
*
* @param st
* The subtype string.
* Case is ignored.
* @return <jk>true</jk> if the subtype contains the specified subtype string.
*/
public final boolean hasSubType(String st) {
if (st != null)
for (String s : subTypes)
if (st.equalsIgnoreCase(s))
return true;
return false;
}
/**
* Returns the subtypes broken down by fragments delimited by <js>"'"</js>.
*
* <P>
* For example, the media type <js>"text/foo+bar"</js> will return a list of
* <code>[<js>'foo'</js>,<js>'bar'</js>]</code>
*
* @return An unmodifiable list of subtype fragments. Never <jk>null</jk>.
*/
public final List<String> getSubTypes() {
return Collections.unmodifiableList(Arrays.asList(subTypes));
}
/**
* Returns <jk>true</jk> if this media type subtype contains the <js>'*'</js> meta character.
*
* @return <jk>true</jk> if this media type subtype contains the <js>'*'</js> meta character.
*/
public final boolean isMetaSubtype() {
return hasSubtypeMeta;
}
/**
* Returns a match metric against the specified media type where a larger number represents a better match.
*
* <p>
* This media type can contain <js>'*'</js> metacharacters.
* <br>The comparison media type must not.
*
* <ul>
* <li>Exact matches (e.g. <js>"text/json"</js>/</js>"text/json"</js>) should match
* better than meta-character matches (e.g. <js>"text/*"</js>/</js>"text/json"</js>)
* <li>The comparison media type can have additional subtype tokens (e.g. <js>"text/json+foo"</js>)
* that will not prevent a match if the <c>allowExtraSubTypes</c> flag is set.
* The reverse is not true, e.g. the comparison media type must contain all subtype tokens found in the
* comparing media type.
* <ul>
* <li>We want the {@link JsonSerializer} (<js>"text/json"</js>) class to be able to handle requests for <js>"text/json+foo"</js>.
* <li>We want to make sure {@link org.apache.juneau.json.SimpleJsonSerializer} (<js>"text/json+simple"</js>) does not handle
* requests for <js>"text/json"</js>.
* </ul>
* More token matches should result in a higher match number.
* </ul>
*
* The formula is as follows for <c>type/subTypes</c>:
* <ul>
* <li>An exact match is <c>100,000</c>.
* <li>Add the following for type (assuming subtype match is &lt;0):
* <ul>
* <li><c>10,000</c> for an exact match (e.g. <js>"text"</js>==<js>"text"</js>).
* <li><c>5,000</c> for a meta match (e.g. <js>"*"</js>==<js>"text"</js>).
* </ul>
* <li>Add the following for subtype (assuming type match is &lt;0):
* <ul>
* <li><c>7,500</c> for an exact match (e.g. <js>"json+foo"</js>==<js>"json+foo"</js> or <js>"json+foo"</js>==<js>"foo+json"</js>)
* <li><c>100</c> for every subtype entry match (e.g. <js>"json"</js>/<js>"json+foo"</js>)
* </ul>
* </ul>
*
* @param o The media type to compare with.
* @param allowExtraSubTypes If <jk>true</jk>,
* @return <jk>true</jk> if the media types match.
*/
public final int match(MediaType o, boolean allowExtraSubTypes) {
if (o == null)
return -1;
// Perfect match
if (this == o || (type.equals(o.type) && subType.equals(o.subType)))
return 100000;
int c = 0;
if (type.equals(o.type))
c += 10000;
else if ("*".equals(type) || "*".equals(o.type))
c += 5000;
if (c == 0)
return 0;
// Subtypes match but are ordered different
if (ArrayUtils.equals(subTypesSorted, o.subTypesSorted))
return c + 7500;
for (String st1 : subTypes) {
if ("*".equals(st1))
c += 0;
else if (ArrayUtils.contains(st1, o.subTypes))
c += 100;
else if (o.hasSubtypeMeta)
c += 0;
else
return 0;
}
for (String st2 : o.subTypes) {
if ("*".equals(st2))
c += 0;
else if (ArrayUtils.contains(st2, subTypes))
c += 100;
else if (hasSubtypeMeta)
c += 0;
else if (! allowExtraSubTypes)
return 0;
else
c += 10;
}
return c;
}
/**
* Returns the additional parameters on this media type.
*
* <p>
* For example, given the media type string <js>"text/html;level=1"</js>, will return a map
* with the single entry <code>{level:[<js>'1'</js>]}</code>.
*
* @return The map of additional parameters, or an empty map if there are no parameters.
*/
public List<NameValuePair> getParameters() {
return Collections.unmodifiableList(Arrays.asList(parameters));
}
/**
* Returns the additional parameter on this media type.
*
* @param name The additional parameter name.
* @return The parameter value, or <jk>null</jk> if not found.
*/
public String getParameter(String name) {
for (NameValuePair p : parameters)
if (eq(name, p.getName()))
return p.getValue();
return null;
}
private static HeaderElement parse(String value) {
HeaderElement[] elements = BasicHeaderValueParser.parseElements(emptyIfNull(trim(value)), null);
return (elements.length > 0 ? elements[0] : new BasicHeaderElement("", ""));
}
@Override /* Object */
public String toString() {
return string;
}
@Override /* Object */
public int hashCode() {
return string.hashCode();
}
@Override /* Object */
public boolean equals(Object o) {
return (o instanceof MediaType) && eq(this, (MediaType)o, (x,y)->eq(x.string, y.string));
}
@Override
public final int compareTo(MediaType o) {
return toString().compareTo(o.toString());
}
}