blob: ea1e8da5fa32435e8c08f0965489ef75e78193bd [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.any23.servlet.conneg;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This class implements the <i>HTTP header media-range specification</i>.
* See <a href="http://www.ietf.org/rfc/rfc2616.txt">RFC 2616 section 14.1</a>.
*/
public class MediaRangeSpec {
private static final Pattern tokenPattern;
private static final Pattern parameterPattern;
private static final Pattern mediaRangePattern;
private static final Pattern qValuePattern;
private final String type;
private final String subtype;
private final List<String> parameterNames;
private final List<String> parameterValues;
private final String mediaType;
private final double quality;
static {
// See RFC 2616, section 2.2
String token = "[\\x20-\\x7E&&[^()<>@,;:\\\"/\\[\\]?={} ]]+";
String quotedString = "\"((?:[\\x20-\\x7E\\n\\r\\t&&[^\"\\\\]]|\\\\[\\x00-\\x7F])*)\"";
// See RFC 2616, section 3.6
String parameter = ";\\s*(?!q\\s*=)(" + token + ")=(?:(" + token + ")|" + quotedString + ")";
// See RFC 2616, section 3.9
String qualityValue = "(?:0(?:\\.\\d{0,3})?|1(?:\\.0{0,3})?)";
// See RFC 2616, sections 14.1
String quality = ";\\s*q\\s*=\\s*([^;,]*)";
// See RFC 2616, section 3.7
String regex = "(" + token + ")/(" + token + ")" +
"((?:\\s*" + parameter + ")*)" +
"(?:\\s*" + quality + ")?" +
"((?:\\s*" + parameter + ")*)";
tokenPattern = Pattern.compile(token);
parameterPattern = Pattern.compile(parameter);
mediaRangePattern = Pattern.compile(regex);
qValuePattern = Pattern.compile(qualityValue);
}
/**
* Parses a media type from a string such as <code>text/html;charset=utf-8;q=0.9</code>.
* @param mediaType input string from which to extract mediaType
* @return {@link org.apache.any23.servlet.conneg.MediaRangeSpec}
*/
public static MediaRangeSpec parseType(String mediaType) {
MediaRangeSpec m = parseRange(mediaType);
if (m == null || m.isWildcardType() || m.isWildcardSubtype()) {
return null;
}
return m;
}
/**
* Parses a media range from a string such as <code>text/*;charset=utf-8;q=0.9</code>.
* Unlike simple media types, media ranges may include wildcards.
* @param mediaRange input string from which to extract media range
* @return {@link org.apache.any23.servlet.conneg.MediaRangeSpec}
*/
public static MediaRangeSpec parseRange(String mediaRange) {
Matcher m = mediaRangePattern.matcher(mediaRange);
if (!m.matches()) {
return null;
}
String type = m.group(1).toLowerCase(Locale.ROOT);
String subtype = m.group(2).toLowerCase(Locale.ROOT);
String unparsedParameters = m.group(3);
String qValue = m.group(7);
m = parameterPattern.matcher(unparsedParameters);
if ("*".equals(type) && !"*".equals(subtype)) {
return null;
}
List<String> parameterNames = new ArrayList<>();
List<String> parameterValues = new ArrayList<>();
while (m.find()) {
String name = m.group(1).toLowerCase(Locale.ROOT);
String value = (m.group(3) == null) ? m.group(2) : unescape(m.group(3));
parameterNames.add(name);
parameterValues.add(value);
}
double quality = 1.0;
if (qValue != null && qValuePattern.matcher(qValue).matches()) {
try {
quality = Double.parseDouble(qValue);
} catch (NumberFormatException ex) {
// quality stays at default value
}
}
return new MediaRangeSpec(type, subtype, parameterNames, parameterValues, quality);
}
/**
* Parses an HTTP Accept header into a List of MediaRangeSpecs
*
* @param s an HTTP accept header.
* @return A List of MediaRangeSpecs
*/
public static List<MediaRangeSpec> parseAccept(String s) {
List<MediaRangeSpec> result = new ArrayList<>();
Matcher m = mediaRangePattern.matcher(s);
while (m.find()) {
result.add(parseRange(m.group()));
}
return result;
}
private static String unescape(String s) {
return s.replaceAll("\\\\(.)", "$1");
}
private static String escape(String s) {
return s.replaceAll("[\\\\\"]", "\\\\$0");
}
private MediaRangeSpec(
String type,
String subtype,
List<String> parameterNames, List<String> parameterValues,
double quality
) {
this.type = type;
this.subtype = subtype;
this.parameterNames = Collections.unmodifiableList(parameterNames);
this.parameterValues = parameterValues;
this.mediaType = buildMediaType();
this.quality = quality;
}
private String buildMediaType() {
StringBuffer result = new StringBuffer();
result.append(type);
result.append("/");
result.append(subtype);
for (int i = 0; i < parameterNames.size(); i++) {
result.append(";");
result.append(parameterNames.get(i));
result.append("=");
String value = parameterValues.get(i);
if (tokenPattern.matcher(value).matches()) {
result.append(value);
} else {
result.append("\"");
result.append(escape(value));
result.append("\"");
}
}
return result.toString();
}
public String getType() {
return type;
}
public String getSubtype() {
return subtype;
}
public String getMediaType() {
return mediaType;
}
public List<String> getParameterNames() {
return parameterNames;
}
public String getParameter(String parameterName) {
for (int i = 0; i < parameterNames.size(); i++) {
if (parameterNames.get(i).equalsIgnoreCase(parameterName)) {
return parameterValues.get(i);
}
}
return null;
}
public boolean isWildcardType() {
return "*".equals(type);
}
public boolean isWildcardSubtype() {
return !isWildcardType() && "*".equals(subtype);
}
public double getQuality() {
return quality;
}
public int getPrecedence(MediaRangeSpec range) {
if (range.isWildcardType())
return 1;
if (!range.type.equals(type))
return 0;
if (range.isWildcardSubtype())
return 2;
if (!range.subtype.equals(subtype))
return 0;
if (range.getParameterNames().isEmpty())
return 3;
int result = 3;
for (int i = 0; i < range.getParameterNames().size(); i++) {
String name = range.getParameterNames().get(i);
String value = range.getParameter(name);
if (!value.equals(getParameter(name)))
return 0;
result++;
}
return result;
}
public MediaRangeSpec getBestMatch(List<MediaRangeSpec> mediaRanges) {
MediaRangeSpec result = null;
int bestPrecedence = 0;
Iterator<MediaRangeSpec> it = mediaRanges.iterator();
while (it.hasNext()) {
MediaRangeSpec range = it.next();
if (getPrecedence(range) > bestPrecedence) {
bestPrecedence = getPrecedence(range);
result = range;
}
}
return result;
}
@Override
public String toString() {
return mediaType + ";q=" + quality;
}
}