blob: 8546f966662bedc95ef7aa71426178b9b993ef22 [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.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;
/**
* This class defines a negotiator for content types based on scoring.
*/
public class ContentTypeNegotiator {
private List<VariantSpec> variantSpecs = new ArrayList<>();
private List<MediaRangeSpec> defaultAcceptRanges = Collections.singletonList(MediaRangeSpec.parseRange("*/*"));
private Collection<AcceptHeaderOverride> userAgentOverrides = new ArrayList<>();
protected ContentTypeNegotiator(){}
/**
* Returns the {@link MediaRangeSpec}
* associated to the given <i>accept</i> type.
*
* @param accept a provided <i>accept</i> type
* @return a {@link MediaRangeSpec} associated to the accept parameter
*/
public MediaRangeSpec getBestMatch(String accept) {
return getBestMatch(accept, null);
}
/**
* Returns the {@link MediaRangeSpec}
* associated to the given <i>accept</i> type and <i>userAgent</i>.
*
* @param accept a provided <i>accept</i> type
* @param userAgent use agent associated with the request
* @return the {@link MediaRangeSpec}
* associated to the given <i>accept</i> type and <i>userAgent</i>.
*/
public MediaRangeSpec getBestMatch(String accept, String userAgent) {
if (userAgent == null) {
userAgent = "";
}
Iterator<AcceptHeaderOverride> it = userAgentOverrides.iterator();
String overriddenAccept = accept;
while (it.hasNext()) {
AcceptHeaderOverride override = it.next();
if (override.matches(accept, userAgent)) {
overriddenAccept = override.getReplacement();
}
}
return new Negotiation(toAcceptRanges(overriddenAccept)).negotiate();
}
protected VariantSpec addVariant(String mediaType) {
VariantSpec result = new VariantSpec(mediaType);
variantSpecs.add(result);
return result;
}
/**
* Sets an Accept header to be used as the default if a client does
* not send an Accept header, or if the Accept header cannot be parsed.
* Defaults to "* / *".
* @param accept a default <i>accept</i> type
*/
protected void setDefaultAccept(String accept) {
this.defaultAcceptRanges = MediaRangeSpec.parseAccept(accept);
}
/**
* Overrides the Accept header for certain user agents. This can be
* used to implement special-case handling for user agents that send
* faulty Accept headers.
*
* @param userAgentString A pattern to be matched against the User-Agent header;
* <code>null</code> means regardless of User-Agent
* @param originalAcceptHeader Only override the Accept header if the user agent
* sends this header; <code>null</code> means always override
* @param newAcceptHeader The Accept header to be used instead
*/
protected void addUserAgentOverride(
Pattern userAgentString,
String originalAcceptHeader,
String newAcceptHeader
) {
this.userAgentOverrides.add(
new AcceptHeaderOverride(userAgentString, originalAcceptHeader, newAcceptHeader)
);
}
private List<MediaRangeSpec> toAcceptRanges(String accept) {
if (accept == null) {
return defaultAcceptRanges;
}
List<MediaRangeSpec> result = MediaRangeSpec.parseAccept(accept);
if (result.isEmpty()) {
return defaultAcceptRanges;
}
return result;
}
protected class VariantSpec {
private MediaRangeSpec type;
private List<MediaRangeSpec> aliases = new ArrayList<>();
private boolean isDefault = false;
public VariantSpec(String mediaType) {
type = MediaRangeSpec.parseType(mediaType);
}
public VariantSpec addAliasMediaType(String mediaType) {
aliases.add(MediaRangeSpec.parseType(mediaType));
return this;
}
public void makeDefault() {
isDefault = true;
}
public MediaRangeSpec getMediaType() {
return type;
}
public boolean isDefault() {
return isDefault;
}
public List<MediaRangeSpec> getAliases() {
return aliases;
}
}
private class Negotiation {
private final List<MediaRangeSpec> ranges;
private MediaRangeSpec bestMatchingVariant = null;
private MediaRangeSpec bestDefaultVariant = null;
private double bestMatchingQuality = 0;
private double bestDefaultQuality = 0;
Negotiation(List<MediaRangeSpec> ranges) {
this.ranges = ranges;
}
MediaRangeSpec negotiate() {
Iterator<VariantSpec> it = variantSpecs.iterator();
while (it.hasNext()) {
VariantSpec variant = it.next();
if (variant.isDefault) {
evaluateDefaultVariant(variant.getMediaType());
}
evaluateVariant(variant.getMediaType());
Iterator<MediaRangeSpec> aliasIt = variant.getAliases().iterator();
while (aliasIt.hasNext()) {
MediaRangeSpec alias = aliasIt.next();
evaluateVariantAlias(alias, variant.getMediaType());
}
}
return (bestMatchingVariant == null) ? bestDefaultVariant : bestMatchingVariant;
}
private void evaluateVariantAlias(MediaRangeSpec variant, MediaRangeSpec isAliasFor) {
if (variant.getBestMatch(ranges) == null)
return;
double q = variant.getBestMatch(ranges).getQuality();
if (q * variant.getQuality() > bestMatchingQuality) {
bestMatchingVariant = isAliasFor;
bestMatchingQuality = q * variant.getQuality();
}
}
private void evaluateVariant(MediaRangeSpec variant) {
evaluateVariantAlias(variant, variant);
}
private void evaluateDefaultVariant(MediaRangeSpec variant) {
if (variant.getQuality() > bestDefaultQuality) {
bestDefaultVariant = variant;
bestDefaultQuality = 0.00001 * variant.getQuality();
}
}
}
private class AcceptHeaderOverride {
private Pattern userAgentPattern;
private String original;
private String replacement;
AcceptHeaderOverride(Pattern userAgentPattern, String original, String replacement) {
this.userAgentPattern = userAgentPattern;
this.original = original;
this.replacement = replacement;
}
boolean matches(String acceptHeader, String userAgentHeader) {
return (userAgentPattern == null
|| userAgentPattern.matcher(userAgentHeader).find())
&& (original == null || original.equals(acceptHeader));
}
String getReplacement() {
return replacement;
}
}
}