VIVO-731 Create ContentTypeUtil with tests
This commit is contained in:
parent
b63e4134ac
commit
7b849ace9b
4 changed files with 533 additions and 0 deletions
|
@ -0,0 +1,18 @@
|
|||
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
|
||||
|
||||
package edu.cornell.mannlib.vitro.webapp.utils.http;
|
||||
|
||||
/**
|
||||
* Indicates an invalid Accept header. Either the basic syntax was flawed, or
|
||||
* the value for "q" could not be parsed to a Float.
|
||||
*/
|
||||
public class AcceptHeaderParsingException extends Exception {
|
||||
public AcceptHeaderParsingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AcceptHeaderParsingException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,241 @@
|
|||
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
|
||||
|
||||
package edu.cornell.mannlib.vitro.webapp.utils.http;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.http.HeaderElement;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.message.BasicHeaderValueParser;
|
||||
|
||||
/**
|
||||
* A utility for selecting content types, in the context of the Accept header.
|
||||
*
|
||||
* -------------------
|
||||
*
|
||||
* This does not support matching against content types with extensions, like
|
||||
* "level=1", as illustrated in RFC-2616:
|
||||
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
|
||||
*
|
||||
* However, as long as we don't offer such extensions on our available types,
|
||||
* the use of extensions in the Accept header is moot.
|
||||
*/
|
||||
public class ContentTypeUtil {
|
||||
|
||||
/**
|
||||
* Given an Accept header value and a list of available type names, decide
|
||||
* which type is the best fit.
|
||||
*
|
||||
* If there is no fit, throw a NotAcceptableException
|
||||
*
|
||||
* The only thing to do is to match all available against all acceptable,
|
||||
* and pick the best match. Try to do as little work as possible inside the
|
||||
* nested loop.
|
||||
*/
|
||||
public static String bestContentType(String acceptHeader,
|
||||
Collection<String> availableTypeNames)
|
||||
throws AcceptHeaderParsingException, NotAcceptableException {
|
||||
if (availableTypeNames == null) {
|
||||
throw new NotAcceptableException("availableTypeNames may not be null.");
|
||||
}
|
||||
|
||||
Set<AcceptableType> acceptableTypes = parseAcceptHeader(acceptHeader);
|
||||
List<MatchCriteria> availableTypes = convertToMatchCriteria(availableTypeNames);
|
||||
|
||||
float bestFitQuality = 0.0F;
|
||||
MatchCriteria bestMatch = null;
|
||||
|
||||
for (AcceptableType acceptableType : acceptableTypes) {
|
||||
for (MatchCriteria availableType : availableTypes) {
|
||||
float fitQuality = acceptableType.fitQuality(availableType);
|
||||
if (fitQuality > bestFitQuality) {
|
||||
bestFitQuality = fitQuality;
|
||||
bestMatch = availableType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestMatch == null) {
|
||||
throw new NotAcceptableException(
|
||||
"No available type matches the Accept header: "
|
||||
+ acceptHeader);
|
||||
} else {
|
||||
return bestMatch.getName();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The order of items in the Accept header is not important. We rely on the
|
||||
* specificity of the match and the "q" factor, in that order.
|
||||
*
|
||||
* Since q ranges between 1.0 and 0.001, we add a specificity offset of 2, 3
|
||||
* or 4. That way, matches with equal specificity are decided by q factor.
|
||||
*/
|
||||
public static Set<AcceptableType> parseAcceptHeader(String acceptHeader)
|
||||
throws AcceptHeaderParsingException {
|
||||
if (acceptHeader == null || acceptHeader.trim().isEmpty()) {
|
||||
return Collections.singleton(new AcceptableType("*/*", "1.0"));
|
||||
}
|
||||
|
||||
HeaderElement[] elements = BasicHeaderValueParser.parseElements(
|
||||
acceptHeader, null);
|
||||
|
||||
Set<AcceptableType> acceptableTypes = new HashSet<>();
|
||||
for (HeaderElement he : elements) {
|
||||
String name = he.getName();
|
||||
|
||||
NameValuePair qPair = he.getParameterByName("q");
|
||||
String qString = (qPair == null) ? "1.0" : qPair.getValue();
|
||||
|
||||
acceptableTypes.add(new AcceptableType(name, qString));
|
||||
}
|
||||
|
||||
return acceptableTypes;
|
||||
}
|
||||
|
||||
private static List<MatchCriteria> convertToMatchCriteria(
|
||||
Collection<String> availableTypes) {
|
||||
List<MatchCriteria> availableMatches = new ArrayList<>();
|
||||
for (String availableType : availableTypes) {
|
||||
availableMatches.add(new MatchCriteria(availableType));
|
||||
}
|
||||
return availableMatches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsing the Accept header returns a set of these.
|
||||
*
|
||||
* Package access to permit unit testing.
|
||||
*/
|
||||
static class AcceptableType {
|
||||
private final MatchCriteria matchCriteria;
|
||||
private final float q;
|
||||
|
||||
public AcceptableType(String name, String qString)
|
||||
throws AcceptHeaderParsingException {
|
||||
this.matchCriteria = new MatchCriteria(name);
|
||||
this.q = parseQValue(qString);
|
||||
}
|
||||
|
||||
private float parseQValue(String qString)
|
||||
throws AcceptHeaderParsingException {
|
||||
float qValue = 0.0F;
|
||||
|
||||
if (qString == null || qString.trim().isEmpty()) {
|
||||
qString = "1";
|
||||
}
|
||||
|
||||
try {
|
||||
qValue = Float.parseFloat(qString);
|
||||
} catch (Exception e) {
|
||||
throw new AcceptHeaderParsingException("invalid q value: '"
|
||||
+ qString + "'");
|
||||
}
|
||||
|
||||
if (qValue > 1.0F || qValue <= 0.0F) {
|
||||
throw new AcceptHeaderParsingException("q value out of range: "
|
||||
+ qString);
|
||||
}
|
||||
|
||||
return qValue;
|
||||
}
|
||||
|
||||
public float fitQuality(MatchCriteria availableType) {
|
||||
int matchQuality = matchCriteria.matchQuality(availableType);
|
||||
if (matchQuality == 0) {
|
||||
return 0;
|
||||
} else {
|
||||
return matchQuality + 1.0F + q;
|
||||
}
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return matchCriteria.getName();
|
||||
}
|
||||
|
||||
public float getQ() {
|
||||
return q;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the available type names into a list of these, so we only do the
|
||||
* substring operations once.
|
||||
*
|
||||
* Package access to permit unit testing.
|
||||
*/
|
||||
static class MatchCriteria {
|
||||
private final String name;
|
||||
private final String type;
|
||||
private final String subtype;
|
||||
|
||||
MatchCriteria(String name) {
|
||||
if (name == null) {
|
||||
name = "";
|
||||
}
|
||||
|
||||
this.name = name;
|
||||
int slashHere = name.indexOf('/');
|
||||
|
||||
if (name.isEmpty()) {
|
||||
this.type = "*";
|
||||
this.subtype = "*";
|
||||
} else if (slashHere == -1) {
|
||||
this.type = name;
|
||||
this.subtype = "*";
|
||||
} else if (slashHere == name.length() - 1) {
|
||||
this.type = name.substring(0, slashHere);
|
||||
this.subtype = "*";
|
||||
} else {
|
||||
this.type = name.substring(0, slashHere);
|
||||
this.subtype = name.substring(slashHere + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If one of the types is a wild-card, it's a weak match.
|
||||
*
|
||||
* Otherwise, if the types match and one of the subtypes is a wild-card,
|
||||
* it's a medium match.
|
||||
*
|
||||
* Otherwise, if the types match and the subtypes match, it's a strong
|
||||
* match.
|
||||
*
|
||||
* Otherwise, it is no match.
|
||||
*/
|
||||
public int matchQuality(MatchCriteria that) {
|
||||
boolean typeMatch = this.type.equals(that.type);
|
||||
boolean typeWild = this.type.equals("*") || that.type.equals("*");
|
||||
boolean subtypeMatch = this.subtype.equals(that.subtype);
|
||||
boolean subtypeWild = this.subtype.equals("*")
|
||||
|| that.subtype.equals("*");
|
||||
|
||||
if (typeWild) {
|
||||
return 1;
|
||||
} else if (typeMatch && subtypeWild) {
|
||||
return 2;
|
||||
} else if (typeMatch && subtypeMatch) {
|
||||
return 3;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getSubtype() {
|
||||
return subtype;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
|
||||
|
||||
package edu.cornell.mannlib.vitro.webapp.utils.http;
|
||||
|
||||
/**
|
||||
* Indicates that none of the available types are acceptable to the client.
|
||||
*/
|
||||
public class NotAcceptableException extends Exception {
|
||||
public NotAcceptableException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,261 @@
|
|||
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
|
||||
|
||||
package edu.cornell.mannlib.vitro.webapp.utils.http;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import edu.cornell.mannlib.vitro.testing.AbstractTestClass;
|
||||
import edu.cornell.mannlib.vitro.webapp.utils.http.ContentTypeUtil.AcceptableType;
|
||||
import edu.cornell.mannlib.vitro.webapp.utils.http.ContentTypeUtil.MatchCriteria;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
public class ContentTypeUtilTest extends AbstractTestClass {
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// MatchCriteria tests
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
public void mcEmptyName() {
|
||||
checkMatchCriteriaConstructor("MC empty name", "", "*", "*");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mcNullName() {
|
||||
checkMatchCriteriaConstructor("MC null name", null, "*", "*");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mcTypeOnly() {
|
||||
checkMatchCriteriaConstructor("MC type only", "image", "image", "*");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mcTypeAndSubtype() {
|
||||
checkMatchCriteriaConstructor("MC type and subtype", "image/png",
|
||||
"image", "png");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mcTypeAndEmptySubtype() {
|
||||
checkMatchCriteriaConstructor("MC type and empty subtype", "image/",
|
||||
"image", "*");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mcWildcardType() {
|
||||
checkMatchCriteriaConstructor("MC wild card type", "*", "*", "*");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mcWildcardSubtype() {
|
||||
checkMatchCriteriaConstructor("MC wild card subtype", "image/*",
|
||||
"image", "*");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mcMatchWildcardType() {
|
||||
checkMatchQuality("MC match wild card type 1", "*", "text", 1);
|
||||
checkMatchQuality("MC match wild card type 2", "text", "*", 1);
|
||||
checkMatchQuality("MC match wild card type 3", "*", "text/plain", 1);
|
||||
checkMatchQuality("MC match wild card type 4", "text/*", "*", 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mcTypesDontMatch() {
|
||||
checkMatchQuality("MC types don't match 1", "this", "that", 0);
|
||||
checkMatchQuality("MC types don't match 2", "this/match", "that/match",
|
||||
0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mcMatchWildcardSubtype() {
|
||||
checkMatchQuality("MC match wild card subtype 1", "text", "text/xml", 2);
|
||||
checkMatchQuality("MC match wild card subtype 2", "image/jpeg",
|
||||
"image/*", 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mcSubtypesDontMatch() {
|
||||
checkMatchQuality("MC match subtypes don't match", "text/xml",
|
||||
"text/plain", 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mcFullMatch() {
|
||||
checkMatchQuality("MC full match", "text/plain", "text/plain", 3);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// AcceptableType tests
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
public void atNullQ() throws AcceptHeaderParsingException {
|
||||
checkAcceptableTypeConstructor("AT null Q", null, 1.0F);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void atEmptyQ() throws AcceptHeaderParsingException {
|
||||
checkAcceptableTypeConstructor("AT empty Q", "", 1.0F);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void atBlankQ() throws AcceptHeaderParsingException {
|
||||
checkAcceptableTypeConstructor("AT blank Q", " \t", 1.0F);
|
||||
}
|
||||
|
||||
@Test(expected = AcceptHeaderParsingException.class)
|
||||
public void atInvalidQ() throws AcceptHeaderParsingException {
|
||||
checkAcceptableTypeConstructor("AT invalid Q", "99XX", 0.0F);
|
||||
}
|
||||
|
||||
@Test(expected = AcceptHeaderParsingException.class)
|
||||
public void atQTooHigh() throws AcceptHeaderParsingException {
|
||||
checkAcceptableTypeConstructor("AT Q too high", "1.1", 0.0F);
|
||||
}
|
||||
|
||||
@Test(expected = AcceptHeaderParsingException.class)
|
||||
public void atQTooLow() throws AcceptHeaderParsingException {
|
||||
checkAcceptableTypeConstructor("AT Q too low", "0", 0.0F);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void atGoodQ() throws AcceptHeaderParsingException {
|
||||
checkAcceptableTypeConstructor("AT good Q", "0.4", 0.4F);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void atWildcardMatchWorks() throws AcceptHeaderParsingException {
|
||||
checkMatchQuality("AT wild card match", "*", 0.5F, "text/plain", 2.5F);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void atPartialMatchIsBetter() throws AcceptHeaderParsingException {
|
||||
checkMatchQuality("AT partial match", "text", 0.5F, "text/plain", 3.5F);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void atFullMatchIsBest() throws AcceptHeaderParsingException {
|
||||
checkMatchQuality("AT full match", "text/plain", 0.5F, "text/plain",
|
||||
4.5F);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void atNoMatchTotallyBites() throws AcceptHeaderParsingException {
|
||||
checkMatchQuality("AT full match", "text/xml", 0.5F, "text/plain", 0.0F);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Best content type tests
|
||||
// ----------------------------------------------------------------------
|
||||
@Test
|
||||
public void ctNullHeaderMatchesAnything() throws Exception {
|
||||
findBestMatch("CT null header matches anything", null,
|
||||
available("anything"), "anything");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ctEmptyHeaderMatchesAnything() throws Exception {
|
||||
findBestMatch("CT empty header matches anything", "",
|
||||
available("anything"), "anything");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ctBlankHeaderMatchesAnything() throws Exception {
|
||||
findBestMatch("CT blank header matches anything", " \t ",
|
||||
available("anything"), "anything");
|
||||
}
|
||||
|
||||
@Test(expected = NotAcceptableException.class)
|
||||
public void ctNullCollectionMatchesNothing() throws Exception {
|
||||
findBestMatch("CT null collection matches nothing", "*/*", null,
|
||||
"nothing");
|
||||
}
|
||||
|
||||
@Test(expected = NotAcceptableException.class)
|
||||
public void ctEmptyCollectionMatchesNothing() throws Exception {
|
||||
findBestMatch("CT empty collection matches nothing", "*/*",
|
||||
available(), "nothing");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ctWildcardIsOK() throws Exception {
|
||||
findBestMatch("CT wild card is OK",
|
||||
"text/*;q=0.3, text/html;q=0.1, */*;q=0.5",
|
||||
available("image/png"), "image/png");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ctPartialMatchIsBetter() throws Exception {
|
||||
findBestMatch("CT partial match is better",
|
||||
"text/*;q=0.3, text/html;q=0.1, */*;q=0.5",
|
||||
available("image/png", "text/xml"), "text/xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ctFullMatchIsBest() throws Exception {
|
||||
findBestMatch("CT full match is best",
|
||||
"text/*;q=0.3, text/html;q=0.1, */*;q=0.5",
|
||||
available("image/png", "text/xml", "text/html"), "text/html");
|
||||
}
|
||||
|
||||
@Test(expected = NotAcceptableException.class)
|
||||
public void ctNoMatchTotalBites() throws Exception {
|
||||
findBestMatch("CT no match totally bites",
|
||||
"text/*;q=0.3, text/html;q=0.1", available("no/match"),
|
||||
"nothing");
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Helper methods
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
private void checkMatchCriteriaConstructor(String message, String name,
|
||||
String expectedType, String expectedSubtype) {
|
||||
MatchCriteria mc = new MatchCriteria(name);
|
||||
assertEquals(message + " - type", expectedType, mc.getType());
|
||||
assertEquals(message + " - subtype", expectedSubtype, mc.getSubtype());
|
||||
}
|
||||
|
||||
private void checkMatchQuality(String message, String name1, String name2,
|
||||
int expected) {
|
||||
MatchCriteria mc1 = new MatchCriteria(name1);
|
||||
MatchCriteria mc2 = new MatchCriteria(name2);
|
||||
int actual = mc1.matchQuality(mc2);
|
||||
assertEquals(message, expected, actual);
|
||||
}
|
||||
|
||||
private void checkAcceptableTypeConstructor(String message, String qString,
|
||||
float expected) throws AcceptHeaderParsingException {
|
||||
AcceptableType at = new AcceptableType("irrelevant", qString);
|
||||
assertEquals(message, expected, at.getQ(), 0.0001F);
|
||||
}
|
||||
|
||||
private void checkMatchQuality(String message, String name1, float qValue,
|
||||
String name2, float expected) throws AcceptHeaderParsingException {
|
||||
AcceptableType at = new AcceptableType(name1, Float.toString(qValue));
|
||||
MatchCriteria mc = new MatchCriteria(name2);
|
||||
float actual = at.fitQuality(mc);
|
||||
assertEquals(message, expected, actual, 0.0001F);
|
||||
}
|
||||
|
||||
private List<String> available(String... names) {
|
||||
return Arrays.asList(names);
|
||||
}
|
||||
|
||||
private void findBestMatch(String message, String acceptHeader,
|
||||
List<String> availableTypeNames, String expected)
|
||||
throws AcceptHeaderParsingException, NotAcceptableException {
|
||||
String actual = ContentTypeUtil.bestContentType(acceptHeader,
|
||||
availableTypeNames);
|
||||
assertEquals(message, expected, actual);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue