org.apache.juneau.http.StringRange Maven / Gradle / Ivy
// ***************************************************************************************************************************
// * 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 java.util.*;
import java.util.Map.*;
import org.apache.juneau.annotation.*;
import org.apache.juneau.internal.*;
/**
* Represents a single value in a comma-delimited header value that optionally contains a quality metric for
* comparison and extension parameters.
*
*
* Similar in concept to {@link MediaTypeRange} except instead of media types (e.g. "text/json" ),
* it's a simple type (e.g. "iso-8601" ).
*
*
* An example of a type range is a value in an Accept-Encoding
header.
*
*
Additional Information
*
*/
@BeanIgnore
public final class StringRange implements Comparable {
private static final StringRange[] DEFAULT = new StringRange[]{new StringRange("*")};
private final String type;
private final Float qValue;
private final Map> extensions;
/**
* Parses a header such as an Accept-Encoding
header value into an array of type ranges.
*
*
* The syntax expected to be found in the referenced value
complies with the syntax described in
* RFC2616, Section 14.1, as described below:
*
* Accept-Encoding = "Accept-Encoding" ":"
* 1#( codings [ ";" "q" "=" qvalue ] )
* codings = ( content-coding | "*" )
*
*
*
* Examples of its use are:
*
* Accept-Encoding: compress, gzip
* Accept-Encoding:
* Accept-Encoding: *
* Accept-Encoding: compress;q=0.5, gzip;q=1.0
* Accept-Encoding: gzip;q=1.0, identity; q=0.5, *;q=0
*
*
* @param value
* The value to parse.
* If null or empty, returns a single TypeRange
is returned that represents all types.
* @return
* The type ranges described by the string.
*
The ranges are sorted such that the most acceptable type is available at ordinal position '0' , and
* the least acceptable at position n-1.
*/
public static StringRange[] parse(String value) {
if (value == null || value.length() == 0)
return DEFAULT;
if (value.indexOf(',') == -1)
return new StringRange[]{new StringRange(value)};
Set ranges = new TreeSet();
for (String r : StringUtils.split(value)) {
r = r.trim();
if (r.isEmpty())
continue;
ranges.add(new StringRange(r));
}
return ranges.toArray(new StringRange[ranges.size()]);
}
@SuppressWarnings("unchecked")
private StringRange(String token) {
Builder b = new Builder(token);
this.type = b.type;
this.qValue = b.qValue;
this.extensions = (b.extensions == null ? Collections.EMPTY_MAP : Collections.unmodifiableMap(b.extensions));
}
private static class Builder {
private String type;
private Float qValue = 1f;
private Map> extensions;
private Builder(String token) {
token = token.trim();
int i = token.indexOf(";q=");
if (i == -1) {
type = token;
return;
}
type = 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());
extensions.get(k).add(v);
} else if (k.equals("q")) {
qValue = new Float(v);
isInExtensions = true;
}
}
}
}
}
}
/**
* Returns the type enclosed by this type range.
*
* Examples:
*
* "compress"
* "gzip"
* "*"
*
*
* @return The type of this type range, lowercased, never null .
*/
public String getType() {
return type;
}
/**
* Returns the 'q' (quality) value for this type, as described in Section 3.9 of RFC2616.
*
*
* The quality value is a float between 0.0
(unacceptable) and 1.0
(most acceptable).
*
*
* If 'q' value doesn't make sense for the context (e.g. this range was extracted from a "content-*"
* header, as opposed to "accept-*" header, its value will always be "1" .
*
* @return The 'q' value for this type, never null .
*/
public Float getQValue() {
return qValue;
}
/**
* Returns the optional set of custom extensions defined for this type.
*
*
* Values are lowercase and never null .
*
* @return The optional list of extensions, never null .
*/
public Map> getExtensions() {
return extensions;
}
/**
* Provides a string representation of this media range, suitable for use as an Accept
header value.
*
*
* The literal text generated will be all lowercase.
*
* @return A media range suitable for use as an Accept header value, never null
.
*/
@Override /* Object */
public String toString() {
StringBuffer sb = new StringBuffer().append(type);
// '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> 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> e : extensions.entrySet()) {
String k = e.getKey();
for (String v : e.getValue())
sb.append(';').append(k).append('=').append(v);
}
}
return sb.toString();
}
/**
* Returns true if the specified object is also a MediaType
, and has the same qValue, type,
* parameters, and extensions.
*
* @return true if object is equivalent.
*/
@Override /* Object */
public boolean equals(Object o) {
if (o == null || !(o instanceof StringRange))
return false;
if (this == o)
return true;
StringRange o2 = (StringRange) o;
return qValue.equals(o2.qValue)
&& type.equals(o2.type)
&& extensions.equals(o2.extensions);
}
/**
* Returns a hash based on this instance's media-type
.
*
* @return A hash based on this instance's media-type
.
*/
@Override /* Object */
public int hashCode() {
return type.hashCode();
}
/**
* Compares two MediaRanges for equality.
*
*
* The values are first compared according to qValue
values.
* Should those values be equal, the type
is then lexicographically compared (case-insensitive) in
* ascending order, with the "*" type demoted last in that order.
* TypeRanges
with the same types but with extensions are promoted over those same types with no
* extensions.
*
* @param o The range to compare to. Never null .
*/
@Override /* Comparable */
public int compareTo(StringRange 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.type.toString().compareTo(type.toString());
return i;
}
/**
* Checks if the specified type matches this range.
*
*
* The type will match this range if the range type string is the same or "*" .
*
* @param type The type to match against this range.
* @return true if the specified type matches this range.
*/
public boolean matches(String type) {
if (qValue == 0)
return false;
return this.type.equals(type) || this.type.equals("*");
}
}