blob: aacafaa08ee5413a3c833cde17353aebba9d3cc3 [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.internal.CollectionUtils.*;
import java.util.*;
import java.util.Map.*;
import java.util.concurrent.*;
import org.apache.juneau.annotation.*;
import org.apache.juneau.internal.*;
/**
* Describes a single 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 RFC2616}
* </ul>
*/
@BeanIgnore
public final class MediaTypeRange implements Comparable<MediaTypeRange> {
private static final MediaTypeRange[] DEFAULT = new MediaTypeRange[]{new MediaTypeRange("*/*")};
private static final boolean NOCACHE = Boolean.getBoolean("juneau.nocache");
private static final ConcurrentHashMap<String,MediaTypeRange[]> CACHE = new ConcurrentHashMap<>();
private final MediaType mediaType;
private final Float qValue;
private final Map<String,Set<String>> extensions;
/**
* Parses an <c>Accept</c> header value into an array of media ranges.
*
* <p>
* The returned media ranges are sorted such that the most acceptable media is available at ordinal position
* <js>'0'</js>, and the least acceptable at position n-1.
*
* <p>
* The syntax expected to be found in the referenced <c>value</c> complies with the syntax described in
* RFC2616, Section 14.1, as described below:
* <p class='bcode w800'>
* Accept = "Accept" ":"
* #( media-range [ accept-params ] )
*
* media-range = ( "*\/*"
* | ( type "/" "*" )
* | ( type "/" subtype )
* ) *( ";" parameter )
* accept-params = ";" "q" "=" qvalue *( accept-extension )
* accept-extension = ";" token [ "=" ( token | quoted-string ) ]
* </p>
*
* @param value
* The value to parse.
* If <jk>null</jk> or empty, returns a single <c>MediaTypeRange</c> is returned that represents all types.
* @return
* The media ranges described by the string.
* The ranges are sorted such that the most acceptable media is available at ordinal position <js>'0'</js>, and
* the least acceptable at position n-1.
*/
public static MediaTypeRange[] parse(String value) {
if (value == null || value.length() == 0)
return DEFAULT;
MediaTypeRange[] mtr = CACHE.get(value);
if (mtr != null)
return mtr;
if (value.indexOf(',') == -1) {
mtr = new MediaTypeRange[]{new MediaTypeRange(value)};
} else {
Set<MediaTypeRange> ranges = new TreeSet<>();
for (String r : StringUtils.split(value)) {
r = r.trim();
if (r.isEmpty())
continue;
ranges.add(new MediaTypeRange(r));
}
mtr = ranges.toArray(new MediaTypeRange[ranges.size()]);
}
if (NOCACHE)
return mtr;
CACHE.putIfAbsent(value, mtr);
return CACHE.get(value);
}
private MediaTypeRange(String token) {
Builder b = new Builder(token);
this.mediaType = b.mediaType;
this.qValue = b.qValue;
this.extensions = unmodifiableMap(b.extensions);
}
static final class Builder {
MediaType mediaType;
Float qValue = 1f;
Map<String,Set<String>> extensions;
Builder(String token) {
token = token.trim();
int i = token.indexOf(";q=");
if (i == -1) {
mediaType = MediaType.forString(token);
return;
}
mediaType = MediaType.forString(token.substring(0, i));
String[] tokens = token.substring(i+1).split(";");
// Only the type of the range is specified
if (tokens.length > 0) {
boolean isInExtensions = false;
for (int j = 0; j < tokens.length; j++) {
String[] parm = tokens[j].split("=");
if (parm.length == 2) {
String k = parm[0], v = parm[1];
if (isInExtensions) {
if (extensions == null)
extensions = new TreeMap<>();
if (! extensions.containsKey(k))
extensions.put(k, new TreeSet<String>());
extensions.get(k).add(v);
} else if (k.equals("q")) {
qValue = new Float(v);
isInExtensions = true;
}
}
}
}
}
}
/**
* Returns the media type enclosed by this media range.
*
* <h5 class='section'>Examples:</h5>
* <ul>
* <li><js>"text/html"</js>
* <li><js>"text/*"</js>
* <li><js>"*\/*"</js>
* </ul>
*
* @return The media type of this media range, lowercased, never <jk>null</jk>.
*/
public MediaType getMediaType() {
return mediaType;
}
/**
* Returns the <js>'q'</js> (quality) value for this type, as described in Section 3.9 of RFC2616.
*
* <p>
* The quality value is a float between <c>0.0</c> (unacceptable) and <c>1.0</c> (most acceptable).
*
* <p>
* If 'q' value doesn't make sense for the context (e.g. this range was extracted from a <js>"content-*"</js>
* header, as opposed to <js>"accept-*"</js> header, its value will always be <js>"1"</js>.
*
* @return The 'q' value for this type, never <jk>null</jk>.
*/
public Float getQValue() {
return qValue;
}
/**
* Returns the optional set of custom extensions defined for this type.
*
* <p>
* Values are lowercase and never <jk>null</jk>.
*
* @return The optional list of extensions, never <jk>null</jk>.
*/
public Map<String,Set<String>> getExtensions() {
return extensions;
}
/**
* Provides a string representation of this media range, suitable for use as an <c>Accept</c> header value.
*
* <p>
* The literal text generated will be all lowercase.
*
* @return A media range suitable for use as an Accept header value, never <c>null</c>.
*/
@Override /* Object */
public String toString() {
StringBuffer sb = new StringBuffer().append(mediaType);
// '1' is equivalent to specifying no qValue. If there's no extensions, then we won't include a qValue.
if (qValue.floatValue() == 1.0) {
if (! extensions.isEmpty()) {
sb.append(";q=").append(qValue);
for (Entry<String,Set<String>> e : extensions.entrySet()) {
String k = e.getKey();
for (String v : e.getValue())
sb.append(';').append(k).append('=').append(v);
}
}
} else {
sb.append(";q=").append(qValue);
for (Entry<String,Set<String>> e : extensions.entrySet()) {
String k = e.getKey();
for (String v : e.getValue())
sb.append(';').append(k).append('=').append(v);
}
}
return sb.toString();
}
/**
* Returns <jk>true</jk> if the specified object is also a <c>MediaType</c>, and has the same qValue, type,
* parameters, and extensions.
*
* @return <jk>true</jk> if object is equivalent.
*/
@Override /* Object */
public boolean equals(Object o) {
if (o == null || !(o instanceof MediaTypeRange))
return false;
if (this == o)
return true;
MediaTypeRange o2 = (MediaTypeRange) o;
return qValue.equals(o2.qValue)
&& mediaType.equals(o2.mediaType)
&& extensions.equals(o2.extensions);
}
/**
* Returns a hash based on this instance's <c>media-type</c>.
*
* @return A hash based on this instance's <c>media-type</c>.
*/
@Override /* Object */
public int hashCode() {
return mediaType.hashCode();
}
/**
* Compares two MediaRanges for equality.
*
* <p>
* The values are first compared according to <c>qValue</c> values.
* Should those values be equal, the <c>type</c> is then lexicographically compared (case-insensitive) in
* ascending order, with the <js>"*"</js> type demoted last in that order.
* <c>MediaRanges</c> with the same type but different sub-types are compared - a more specific subtype is
* promoted over the 'wildcard' subtype.
* <c>MediaRanges</c> with the same types but with extensions are promoted over those same types with no
* extensions.
*
* @param o The range to compare to. Never <jk>null</jk>.
*/
@Override /* Comparable */
public int compareTo(MediaTypeRange o) {
// Compare q-values.
int qCompare = Float.compare(o.qValue, qValue);
if (qCompare != 0)
return qCompare;
// Compare media-types.
// Note that '*' comes alphabetically before letters, so just do a reverse-alphabetical comparison.
int i = o.mediaType.toString().compareTo(mediaType.toString());
return i;
}
}