/* Copyright (c) 2006 Google Inc. With trivial modifications (marked MCA) for Votorola.
*
* 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 votorola.g.web;
//import com.google.gdata.client.Service;
import java.util.HashMap;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Simple class for parsing and generating Content-Type header values, per
* RFC 2045 (MIME) and 2616 (HTTP 1.1).
*
*
*/
public class ContentType {
private static String TOKEN =
"[\\p{ASCII}&&[^\\p{Cntrl} ;/=\\[\\]\\(\\)\\<\\>\\@\\,\\:\\\"\\?\\=]]+";
// Precisely matches a token
private static Pattern TOKEN_PATTERN = Pattern.compile(
"^" + TOKEN + "$");
// Matches a media type value
private static Pattern TYPE_PATTERN = Pattern.compile(
"(" + TOKEN + ")" + // type (G1)
"/" + // separator
"(" + TOKEN + ")" + // subtype (G2)
"\\s*(.*)\\s*", Pattern.DOTALL);
// Matches an attribute value
private static Pattern ATTR_PATTERN = Pattern.compile(
"\\s*;\\s*" +
"(" + TOKEN + ")" + // attr name (G1)
"\\s*=\\s*" +
"(?:" +
"\"([^\"]*)\"" + // value as quoted string (G3)
"|" +
"(" + TOKEN + ")?" + // value as token (G2)
")"
);
/**
* Name of the attribute that contains the encoding character set for
* the content type.
* @see #getCharset()
*/
public static final String ATTR_CHARSET = "charset";
/**
* Special "*" character to match any type or subtype.
*/
private static final String STAR = "*";
/**
* The UTF-8 charset encoding is used by default for all text and xml
* based MIME types.
*/
private static final String DEFAULT_CHARSET = ATTR_CHARSET + "=UTF-8";
/**
* A ContentType constant that describes the base unqualified Atom content
* type.
*/
public static final ContentType ATOM =
new ContentType("application/atom+xml;" + DEFAULT_CHARSET);
/**
* A ContentType constant that describes the qualified Atom entry content
* type.
*
* @see #getAtomEntry()
*/
static final ContentType ATOM_ENTRY =
new ContentType("application/atom+xml;type=entry;" + DEFAULT_CHARSET) {
@Override
public boolean match(ContentType acceptedContentType) {
String type = acceptedContentType.getAttribute("type");
return super.match(acceptedContentType) &&
(type == null || type.equals("entry"));
}
};
/**
* A ContentType constant that describes the qualified Atom feed content
* type.
*
* @see #getAtomFeed()
*/
static final ContentType ATOM_FEED =
new ContentType("application/atom+xml;type=feed;" + DEFAULT_CHARSET) {
@Override
public boolean match(ContentType acceptedContentType) {
String type = acceptedContentType.getAttribute("type");
return super.match(acceptedContentType) &&
(type == null || type.equals("feed"));
}
};
/**
* Returns the ContentType that should be used in contexts that expect
* an Atom entry.
*
* @throws UnsupportedOperationException
*/
public static ContentType getAtomEntry() {
// Use the unqualifed type for v1, the qualifed one for later versions
// return Service.getVersion().isCompatible(Service.Versions.V1) ?
// ATOM : ATOM_ENTRY;
//// MCA:
throw new UnsupportedOperationException();
}
/**
* Returns the ContentType that should be used in contexts that expect
* an Atom feed.
*
* @throws UnsupportedOperationException
*/
public static ContentType getAtomFeed() {
// Use the unqualified type for v1, the qualified one for later versions
// return Service.getVersion().isCompatible(Service.Versions.V1) ?
// ATOM : ATOM_FEED;
//// MCA:
throw new UnsupportedOperationException();
}
/**
* A ContentType constant that describes the Atom Service content type.
*/
public static final ContentType ATOM_SERVICE =
new ContentType("application/atomsvc+xml;" + DEFAULT_CHARSET);
/**
* A ContentType constant that describes the RSS channel/item content type.
*/
public static final ContentType RSS =
new ContentType("application/rss+xml;" + DEFAULT_CHARSET);
/**
* A ContentType constant that describes the JSON content type.
*/
public static final ContentType JSON =
new ContentType("application/json;" + DEFAULT_CHARSET);
/**
* A ContentType constant that describes the JavaScript content type.
*/
public static final ContentType JAVASCRIPT =
new ContentType("text/javascript;" + DEFAULT_CHARSET);
/**
* A ContentType constant that describes the generic text/xml content type.
*/
public static final ContentType TEXT_XML =
new ContentType("text/xml;" + DEFAULT_CHARSET);
/**
* A ContentType constant that describes the generic text/html content type.
*/
public static final ContentType TEXT_HTML =
new ContentType("text/html;" + DEFAULT_CHARSET);
/**
* A ContentType constant that describes the generic text/plain content type.
*/
public static final ContentType TEXT_PLAIN =
new ContentType("text/plain;" + DEFAULT_CHARSET);
/**
* A ContentType constant that describes the MIME multipart/related content
* type.
*/
public static final ContentType MULTIPART_RELATED =
new ContentType("multipart/related");
/**
* Determines the best "Content-Type" header to use in a servlet response
* based on the "Accept" header from a servlet request.
*
* @param acceptHeader "Accept" header value from a servlet request (not
* null
)
* @param actualContentTypes actual content types in descending order of
* preference (non-empty, and each entry is of the
* form "type/subtype" without the wildcard char
* '*') or null
if no "Accept" header
* was specified
* @return the best content type to use (or null
on no match).
*/
public static ContentType getBestContentType(String acceptHeader,
List actualContentTypes) {
// If not accept header is specified, return the first actual type
if (acceptHeader == null) {
return actualContentTypes.get(0);
}
// iterate over all of the accepted content types to find the best match
float bestQ = 0;
ContentType bestContentType = null;
String[] acceptedTypes = acceptHeader.split(",");
for (String acceptedTypeString : acceptedTypes) {
// create the content type object
ContentType acceptedContentType;
try {
acceptedContentType = new ContentType(acceptedTypeString.trim());
} catch (IllegalArgumentException ex) {
// ignore exception
continue;
}
// parse the "q" value (default of 1)
float curQ = 1;
try {
String qAttr = acceptedContentType.getAttribute("q");
if (qAttr != null) {
float qValue = Float.valueOf(qAttr);
if (qValue <= 0 || qValue > 1) {
continue;
}
curQ = qValue;
}
} catch (NumberFormatException ex) {
// ignore exception
continue;
}
// only check it if it's at least as good ("q") as the best one so far
if (curQ < bestQ) {
continue;
}
/* iterate over the actual content types in order to find the best match
to the current accepted content type */
for (ContentType actualContentType : actualContentTypes) {
/* if the "q" value is the same as the current best, only check for
better content types */
if (curQ == bestQ && bestContentType == actualContentType) {
break;
}
/* check if the accepted content type matches the current actual
content type */
if (actualContentType.match(acceptedContentType)) {
bestContentType = actualContentType;
bestQ = curQ;
break;
}
}
}
// if found an acceptable content type, return the best one
if (bestQ != 0) {
return bestContentType;
}
// Return null if no match
return null;
}
/**
* Constructs a new instance with default media type
*/
public ContentType() {
this(null);
}
/**
* Constructs a new instance from a content-type header value
* parsing the MIME content type (RFC2045) format. If the type
* is {@code null}, then media type and charset will be
* initialized to default values.
*
* @param typeHeader content type value in RFC2045 header format.
*/
public ContentType(String typeHeader) {
// If the type header is no provided, then use the HTTP defaults.
if (typeHeader == null) {
type = "application";
subType = "octet-stream";
attributes.put(ATTR_CHARSET, "iso-8859-1"); // http default
return;
}
// Get type and subtype
Matcher typeMatch = TYPE_PATTERN.matcher(typeHeader);
if (!typeMatch.matches()) {
throw new IllegalArgumentException("Invalid media type:" + typeHeader);
}
type = typeMatch.group(1).toLowerCase();
subType = typeMatch.group(2).toLowerCase();
if (typeMatch.groupCount() < 3) {
return;
}
// Get attributes (if any)
Matcher attrMatch = ATTR_PATTERN.matcher(typeMatch.group(3));
while (attrMatch.find()) {
String value = attrMatch.group(2);
if (value == null) {
value = attrMatch.group(3);
if (value == null) {
value = "";
}
}
attributes.put(attrMatch.group(1).toLowerCase(), value);
}
// Infer a default charset encoding if unspecified.
if (!attributes.containsKey(ATTR_CHARSET)) {
inferredCharset = true;
if (subType.endsWith("xml")) {
if (type.equals("application")) {
// BUGBUG: Actually have need to look at the raw stream here, but
// if client omitted the charset for "application/xml", they are
// ignoring the STRONGLY RECOMMEND language in RFC 3023, sec 3.2.
// I have little sympathy.
attributes.put(ATTR_CHARSET, "utf-8"); // best guess
} else {
attributes.put(ATTR_CHARSET, "us-ascii"); // RFC3023, sec 3.1
}
} else if (subType.equals("json")) {
attributes.put(ATTR_CHARSET, "utf-8"); // RFC4627, sec 3
} else {
attributes.put(ATTR_CHARSET, "iso-8859-1"); // http default
}
}
}
/** {code True} if parsed input didn't contain charset encoding info */
private boolean inferredCharset = false;
private String type;
public String getType() { return type; }
public void setType(String type) { this.type = type; }
private String subType;
public String getSubType() { return subType; }
public void setSubType(String subType) { this.subType = subType; }
/** Returns the full media type */
public String getMediaType() {
StringBuilder sb = new StringBuilder(); // MCA instead of StringBuffer
sb.append(type);
sb.append("/");
sb.append(subType);
return sb.toString();
}
private HashMap attributes = new HashMap();
/**
* Returns the additional attributes of the content type.
*/
public HashMap getAttributes() { return attributes; }
/**
* Returns the additional attribute by name of the content type.
*
* @param name attribute name
*/
public String getAttribute(String name) {
return attributes.get(name);
}
/*
* Returns the charset attribute of the content type or null if the
* attribute has not been set.
*/
public String getCharset() { return attributes.get(ATTR_CHARSET); }
/**
* Returns whether this content type is match by the content type found in the
* "Accept" header field of an HTTP request.
*
* @param acceptedContentType content type found in the "Accept" header field
* of an HTTP request
*/
public boolean match(ContentType acceptedContentType) {
String acceptedType = acceptedContentType.getType();
String acceptedSubType = acceptedContentType.getSubType();
return STAR.equals(acceptedType) || type.equals(acceptedType) &&
(STAR.equals(acceptedSubType) || subType.equals(acceptedSubType));
}
/**
* Generates the Content-Type value
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder(); // MCA instead of StringBuffer
sb.append(type);
sb.append("/");
sb.append(subType);
for (String name : attributes.keySet()) {
// Don't include any inferred charset attribute in output.
if (inferredCharset && ATTR_CHARSET.equals(name)) {
continue;
}
sb.append(";");
sb.append(name);
sb.append("=");
String value = attributes.get(name);
Matcher tokenMatcher = TOKEN_PATTERN.matcher(value);
if (tokenMatcher.matches()) {
sb.append(value);
} else {
sb.append("\"" + value + "\"");
}
}
return sb.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ContentType that = (ContentType) o;
return type.equals(that.type) && subType.equals(that.subType) && attributes
.equals(that.attributes);
}
@Override
public int hashCode() {
return (type.hashCode() * 31 + subType.hashCode()) * 31 + attributes
.hashCode();
}
}