blob: 376fdee6da362ac94e29d07dbe5119ad06ce2385 [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;
import java.util.*;
import java.util.Map;
import java.util.Map.*;
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).
*/
@BeanIgnore
public final class MediaRange implements Comparable<MediaRange> {
private static final MediaRange[] DEFAULT = new MediaRange[]{new MediaRange("*/*")};
private final MediaType mediaType;
private final Float qValue;
private final Map<String,Set<String>> parameters, extensions;
/**
* Parses a media range fragement of an <code>Accept</code> header value into a single media range object..
* <p>
* The syntax expected to be found in the referenced <code>value</code> complies with the syntax described in RFC2616, Section 14.1, as described below:
* <p class='bcode'>
* media-range = ( "*\/*"
* | ( type "/" "*" )
* | ( type "/" subtype )
* ) *( ";" parameter )
* accept-params = ";" "q" "=" qvalue *( accept-extension )
* accept-extension = ";" token [ "=" ( token | quoted-string ) ]
* </p>
* @param mediaRangeFragment The media range fragement string.
*/
private MediaRange(String mediaRangeFragment) {
String r = mediaRangeFragment;
Float _qValue = 1f;
MediaType _mediaType = null;
Map<String,Set<String>> _parameters = null;
Map<String,Set<String>> _extensions = null;
r = r.trim();
int i = r.indexOf(';');
if (i == -1) {
_mediaType = MediaType.forString(r);
} else {
_mediaType = MediaType.forString(r.substring(0, i));
String[] tokens = r.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<String,Set<String>>();
if (! _extensions.containsKey(parm[0]))
_extensions.put(parm[0], new TreeSet<String>());
_extensions.get(parm[0]).add(parm[1]);
} else if (k.equals("q")) {
_qValue = new Float(v);
isInExtensions = true;
} else /*(! isInExtensions)*/ {
if (_parameters == null)
_parameters = new TreeMap<String,Set<String>>();
if (! _parameters.containsKey(parm[0]))
_parameters.put(parm[0], new TreeSet<String>());
_parameters.get(parm[0]).add(parm[1]);
}
}
}
}
}
if (_parameters == null)
_parameters = Collections.emptyMap();
if (_extensions == null)
_extensions = Collections.emptyMap();
this.mediaType = _mediaType;
this.parameters = _parameters;
this.qValue = _qValue;
this.extensions = _extensions;
}
/**
* 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 <code>0.0</code> (unacceptable) and <code>1.0</code> (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 parameters associated to the type as returned by {@link #getMediaType()}.
* <p>
* The parameters are those values as described in standardized MIME syntax.
* An example of such a parameter in string form might be <js>"level=1"</js>.
* <p>
* Values are lowercase and never <jk>null</jk>.
*
* @return The optional list of parameters, never <jk>null</jk>.
*/
public Map<String,Set<String>> getParameters() {
return parameters;
}
/**
* 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 <code>Accept</code> header value.
* <p>
* The literal text generated will be all lowercase.
*
* @return A media range suitable for use as an Accept header value, never <code>null</code>.
*/
@Override /* Object */
public String toString() {
StringBuffer sb = new StringBuffer().append(mediaType);
for (Entry<String,Set<String>> e : parameters.entrySet()) {
String k = e.getKey();
for (String v : e.getValue())
sb.append(';').append(k).append('=').append(v);
}
// '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 <code>MediaType</code>, 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 MediaRange))
return false;
if (this == o)
return true;
MediaRange o2 = (MediaRange) o;
return qValue.equals(o2.qValue)
&& mediaType.equals(o2.mediaType)
&& parameters.equals(o2.parameters)
&& extensions.equals(o2.extensions);
}
/**
* Returns a hash based on this instance's <code>media-type</code>.
*
* @return A hash based on this instance's <code>media-type</code>.
*/
@Override /* Object */
public int hashCode() {
return mediaType.hashCode();
}
/**
* Parses an <code>Accept</code> 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 <code>value</code> complies with the syntax described in RFC2616, Section 14.1, as described below:
* <p class='bcode'>
* 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>
* This method can also be used on other headers such as <code>Accept-Charset</code> and <code>Accept-Encoding</code>...
* <p class='bcode'>
* Accept-Charset = "Accept-Charset" ":"
* 1#( ( charset | "*" )[ ";" "q" "=" qvalue ] )
* </p>
*
* @param value The value to parse. If <jk>null</jk> or empty, returns a single <code>MediaRange</code> 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 MediaRange[] parse(String value) {
if (value == null || value.length() == 0)
return DEFAULT;
value = value.toLowerCase(Locale.ENGLISH);
if (value.indexOf(',') == -1)
return new MediaRange[]{new MediaRange(value)};
Set<MediaRange> ranges = new TreeSet<MediaRange>();
for (String r : StringUtils.split(value, ',')) {
r = r.trim();
if (r.isEmpty())
continue;
ranges.add(new MediaRange(r));
}
return ranges.toArray(new MediaRange[ranges.size()]);
}
/**
* Compares two MediaRanges for equality.
* <p>
* The values are first compared according to <code>qValue</code> values.
* Should those values be equal, the <code>type</code> is then lexicographically compared (case-insensitive) in ascending order,
* with the <js>"*"</js> type demoted last in that order.
* <code>MediaRanges</code> with the same type but different sub-types are compared - a more specific subtype is
* promoted over the 'wildcard' subtype.
* <code>MediaRanges</code> 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(MediaRange 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;
}
/**
* Matches the specified media type against this range and returns a q-value
* between 0 and 1 indicating the quality of the match.
*
* @param o The media type to match against.
* @return A float between 0 and 1. 1 is a perfect match. 0 is no match at all.
*/
public float matches(MediaType o) {
if (this.mediaType == o || mediaType.matches(o))
return qValue;
return 0;
}
}