![JAR search and dependency download from the Maven repository](/logo.png)
net.officefloor.web.accept.AcceptNegotiatorImpl Maven / Gradle / Ivy
/*
* OfficeFloor - http://www.officefloor.net
* Copyright (C) 2005-2018 Daniel Sagenschneider
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
package net.officefloor.web.accept;
import java.util.ArrayList;
import java.util.List;
import net.officefloor.server.http.HttpException;
import net.officefloor.server.http.HttpHeader;
import net.officefloor.server.http.HttpRequest;
import net.officefloor.server.http.HttpRequestHeaders;
import net.officefloor.server.http.ServerHttpConnection;
/**
* {@link AcceptNegotiator} implementation.
*
* @author Daniel Sagenschneider
*/
public class AcceptNegotiatorImpl implements AcceptNegotiator {
/**
* Accept handler.
*/
public static class AcceptHandler {
/**
* Type of {@link AcceptHandler}.
*/
private final AcceptHandlerEnum type;
/**
* Content-Type
for matching. Value specific to
* {@link AcceptHandlerEnum}.
*/
private final String matchContentType;
/**
* Handler.
*/
private final H handler;
/**
* Instantiate.
*
* @param handler
* Handler.
*/
private AcceptHandler(AcceptHandlerEnum type, String matchContentType, H handler) {
this.type = type;
this.matchContentType = matchContentType;
this.handler = handler;
}
}
/**
* Easy look up of
*
*
* @author Daniel Sagenschneider
*/
private static enum AcceptHandlerEnum {
SUB_TYPE, TYPE, ANY
}
/**
* Creates the {@link AcceptHandler}.
*
* @param
* Handle type.
* @param contentType
* Content-Type
* @param handler
* Handler.
* @return {@link AcceptHandler}.
*/
public static AcceptHandler createAcceptHandler(String contentType, H handler) {
// Clean content type
contentType = contentType.trim();
// Determine if default content type
if ("*/*".equals(contentType)) {
return new AcceptHandler(AcceptHandlerEnum.ANY, null, handler);
}
// Determine if type (with wildcard sub type)
if (contentType.endsWith("/*")) {
return new AcceptHandler(AcceptHandlerEnum.TYPE, contentType.split("/")[0] + "/", handler);
}
// As here, is specific type
return new AcceptHandler(AcceptHandlerEnum.SUB_TYPE, contentType, handler);
}
/**
* {@link AcceptType} linked list to use should there be no accept
* {@link HttpHeader} values.
*/
private static final AcceptType MATCH_ANY = new AnyAcceptType("1", 0);
/**
* {@link AcceptHandler} instances.
*/
private final AcceptHandler[] acceptHandlers;
/**
* Default {@link AcceptHandler}.
*/
private final AcceptHandler defaultAcceptHandler;
/**
* Instantiate.
*
* @param acceptHandlers
* {@link AcceptHandler} instances.
*/
@SuppressWarnings("unchecked")
public AcceptNegotiatorImpl(AcceptHandler[] acceptHandlers) {
// Split into lists
AcceptHandler defaultAcceptHandler = null;
List> handlers = new ArrayList<>(acceptHandlers.length);
for (AcceptHandler handler : acceptHandlers) {
switch (handler.type) {
case ANY:
// Only one default matcher allowed
if (defaultAcceptHandler != null) {
throw new IllegalStateException("Two default (*/*) handlers configured");
}
defaultAcceptHandler = handler;
break;
default:
// Include remaining
handlers.add(handler);
break;
}
}
// Sort the accept handlers
handlers.sort((a, b) -> {
int comparison = a.type.ordinal() - b.type.ordinal();
if (comparison == 0) {
// Match, so sort by content type (descending)
return a.matchContentType.compareTo(b.matchContentType) * -1;
}
return comparison;
});
// Configure
this.acceptHandlers = handlers.toArray(new AcceptHandler[handlers.size()]);
this.defaultAcceptHandler = defaultAcceptHandler;
}
/*
* ================== AcceptNegotiator ====================
*/
@Override
public H getHandler(HttpRequest request) {
// Parse out the accept type
AcceptType acceptType = parseAccept(request);
// Find first matching handler
while (acceptType != null) {
// Attempt to match to accept handler
for (int i = 0; i < this.acceptHandlers.length; i++) {
AcceptHandler handler = this.acceptHandlers[i];
if (acceptType.isMatch(handler)) {
// Found handler
return handler.handler;
}
}
// Try next accept type
acceptType = acceptType.next;
}
// Determine if default match
if (this.defaultAcceptHandler != null) {
return this.defaultAcceptHandler.handler;
}
// As here, no match found
return null;
}
/**
* Parses the {@link AcceptType} linked list from the
* {@link ServerHttpConnection}.
*
* @param request
* {@link HttpRequest}.
* @return Head {@link AcceptType} of the linked list.
*/
private static AcceptType parseAccept(HttpRequest request) {
// Accept type
AcceptType head = null;
// Load the accept types
HttpRequestHeaders headers = request.getHeaders();
for (HttpHeader header : headers.getHeaders("accept")) {
head = parseAccept(header.getValue(), head);
}
// Determine if only wild card match
// - no head, so will match any type
// - only one head that is any match, so will match any type
boolean isOnlyWildcard = ((head == null) || ((head.next == null) && (head.getClass() == AnyAcceptType.class)));
// Default to content-type if wild card only
if (isOnlyWildcard) {
// Attempt to match first on input content type
// (e.g. if JSON sent then respond with JSON)
HttpHeader contentTypeHeader = headers.getHeader("content-type");
if (contentTypeHeader != null) {
head = new SubTypeAcceptType(contentTypeHeader.getValue(), "1", 0);
}
// Now match any
if (head == null) {
head = MATCH_ANY;
} else {
head.next = MATCH_ANY;
}
}
// Return the head of the linked list
return head;
}
/**
* State of parsing.
*/
private static enum ParseState {
NEW_ACCEPT, TYPE, SUB_TYPE, PARAMETER_START, PARAMETER_NAME, PARAMETER_VALUE_START, PARAMETER_VALUE
}
/**
* Indicates if the character is a white space.
*
* @param character
* Character.
* @return true
if character is white space.
*/
private static final boolean isWhiteSpace(char character) {
return (character == ' ') || (character == '\t');
}
/**
* Parses the accept
{@link HttpHeader} value returning the head
* {@link AcceptType} of the linked list of {@link AcceptType} instances.
*
* @param accept
* accept
{@link HttpHeader} value.
* @param head
* Head {@link AcceptType} from another
* accept {@link HttpHeader} should there be multiple accept
* {@link HttpHeader} values. Will be null
if no other
* accept
{@link HttpHeader}.
* @return Head {@link AcceptType} for parsed out linked list of
* {@link AcceptType} instances. The values are sorted with highest
* weighted first.
* @throws HttpException
* If invalid accept
value.
*/
private static final AcceptType parseAccept(String accept, AcceptType head) throws HttpException {
// State for parsing
ParseState state = ParseState.NEW_ACCEPT;
int typeStart = -1;
int typeSeparatorPosition = -1;
int subTypeEnd = -1;
int paramStart = -1;
int paramEnd = -1;
boolean isParamEnd = false;
boolean isQ = false;
String q = "0";
int parameterCount = 0;
// Parse out the accept types
NEXT_CHARACTER: for (int index = 0; index < accept.length(); index++) {
char character = accept.charAt(index);
// Handle based on state
switch (state) {
case NEW_ACCEPT:
// Determine if load previous accept type
if (typeStart != -1) {
// Load the previous accept type
head = loadAcceptType(accept, typeStart, typeSeparatorPosition, subTypeEnd, q, parameterCount,
head);
// Reset if multiple spaces
typeStart = -1;
}
// Ignore leading space
if (isWhiteSpace(character)) {
continue NEXT_CHARACTER;
}
// Start of type
typeStart = index;
subTypeEnd = -1; // reset to find
q = "0";
parameterCount = 0; // reset for new accept type
state = ParseState.TYPE;
break;
case TYPE:
// Look for end of type
if (character == '/') {
// Separator between type/sub-type
typeSeparatorPosition = index;
state = ParseState.SUB_TYPE;
}
break;
case SUB_TYPE:
// Determine if terminated by space
if (isWhiteSpace(character)) {
// Ensure not multiple spaces
if (subTypeEnd == -1) {
subTypeEnd = index;
}
} else if (character == ';') {
// Starting parameter
if (subTypeEnd == -1) {
subTypeEnd = index;
}
state = ParseState.PARAMETER_START;
} else if (character == ',') {
// No parameters for accept type
if (subTypeEnd == -1) {
subTypeEnd = index;
}
// Start new accept
state = ParseState.NEW_ACCEPT;
}
break;
case PARAMETER_START:
// Ignore leading space
if (isWhiteSpace(character)) {
continue NEXT_CHARACTER;
}
// Start of parameter name
paramStart = index;
paramEnd = -1; // reset to find
isQ = false; // reset to determine
state = ParseState.PARAMETER_NAME;
break;
case PARAMETER_NAME:
// Determine if terminated by space
isParamEnd = false;
if (isWhiteSpace(character)) {
// Ensure not multiple spaces
if (paramEnd == -1) {
paramEnd = index;
isParamEnd = true;
}
} else if (character == '=') {
// Parameter with value
if (paramEnd == -1) {
paramEnd = index;
isParamEnd = true;
}
state = ParseState.PARAMETER_VALUE_START;
} else if (character == ';') {
// Parameter without value
if (paramEnd == -1) {
paramEnd = index;
isParamEnd = true;
}
state = ParseState.PARAMETER_START;
} else if (character == ',') {
// Parameter without value, and no more parameters
if (paramEnd == -1) {
paramEnd = index;
isParamEnd = true;
}
state = ParseState.NEW_ACCEPT;
}
if (isParamEnd) {
// Have another parameter
parameterCount++;
// Found end of parameter name, so determine if q
if ((paramEnd - paramStart) == 1) { // "q"
isQ = accept.charAt(paramStart) == 'q';
}
}
break;
case PARAMETER_VALUE_START:
// Ignore leading space
if (isWhiteSpace(character)) {
continue NEXT_CHARACTER;
}
// Start of parameter name
paramStart = index;
paramEnd = -1; // reset to find
state = ParseState.PARAMETER_VALUE;
break;
case PARAMETER_VALUE:
// Determine if terminated by space
isParamEnd = false;
if (isWhiteSpace(character)) {
// Ensure not multiple spaces
if (paramEnd == -1) {
paramEnd = index;
isParamEnd = true;
}
} else if (character == ';') {
// Another parameter
if (paramEnd == -1) {
paramEnd = index;
isParamEnd = true;
}
state = ParseState.PARAMETER_START;
} else if (character == ',') {
// No more parameters
if (paramEnd == -1) {
paramEnd = index;
isParamEnd = true;
}
state = ParseState.NEW_ACCEPT;
}
if (isParamEnd && isQ) {
// Found q value
q = accept.substring(paramStart, paramEnd);
if (q.length() == 0) {
// No value, so assume lowest
q = "0";
} else if (q.charAt(0) == '.') {
// Prefix with 0 to allow string sorting
q = "0" + q;
}
}
break;
}
}
// Handle reached end of accept value
switch (state) {
case NEW_ACCEPT:
case TYPE:
break;
case SUB_TYPE:
// Load the default accept type
head = loadAcceptType(accept, typeStart, typeSeparatorPosition, accept.length(), "0", 0, head);
break;
case PARAMETER_NAME:
parameterCount++; // include last parameter
// carry on to load accept type
case PARAMETER_START:
case PARAMETER_VALUE_START:
// Just parameter name, so no check for q ending parameter
head = loadAcceptType(accept, typeStart, typeSeparatorPosition, subTypeEnd, q, parameterCount, head);
break;
case PARAMETER_VALUE:
// Check if last parameter is q
if ((paramEnd == -1) && isQ) {
q = accept.substring(paramStart, accept.length());
}
head = loadAcceptType(accept, typeStart, typeSeparatorPosition, subTypeEnd, q, parameterCount, head);
break;
}
return head;
}
/**
* Loads the {@link AcceptType} to the linked list, returning the head of the
* linked list.
*
* @param accept
* accept
{@link HttpHeader} value.
* @param typeStart
* Start of type.
* @param typeSeparatorPosition
* Position of / separating type and sub-type.
* @param subTypeEnd
* End of sub-type.
* @param q
* q
value.
* @param parameterCount
* Number of parameters.
* @param head
* Previous head {@link AcceptType} of the linked list.
* @return Potentially new head {@link AcceptType} of the linked list.
*/
private static final AcceptType loadAcceptType(String accept, int typeStart, int typeSeparatorPosition,
int subTypeEnd, String q, int parameterCount, AcceptType head) {
// Determine if wild card match
if ((subTypeEnd - typeStart) == 3) { // "*/*"
// Potentially wild card match
boolean isTypeWild = accept.charAt(typeStart) == '*';
boolean isSubTypeWild = accept.charAt(subTypeEnd - 1) == '*';
if (isTypeWild && isSubTypeWild) {
// Accept any content type
return appendAcceptType(head, new AnyAcceptType(q, parameterCount));
} else if (isSubTypeWild) {
// Accept specific type and any sub type
String typePrefix = accept.substring(typeStart, typeSeparatorPosition);
return appendAcceptType(head, new TypeAcceptType(typePrefix, q, parameterCount));
}
} else if ((subTypeEnd - (typeSeparatorPosition + 1)) == 1) { // "*"
// Potentially sub type wild card match
boolean isSubTypeWild = accept.charAt(subTypeEnd - 1) == '*';
if (isSubTypeWild) {
// Accept specific type and any sub type (+1 to include /)
String typePrefix = accept.substring(typeStart, (typeSeparatorPosition + 1));
return appendAcceptType(head, new TypeAcceptType(typePrefix, q, parameterCount));
}
}
// Accept specific type and specific sub type
String contentType = accept.substring(typeStart, subTypeEnd);
return appendAcceptType(head, new SubTypeAcceptType(contentType, q, parameterCount));
}
/**
* Appends the {@link AcceptType} into the linked list.
*
* @param head
* Previous head {@link AcceptType} of the linked list.
* @param newAccept
* {@link AcceptType} to add.
* @return Potentially new head {@link AcceptType} of the linked list.
*/
private static final AcceptType appendAcceptType(AcceptType head, AcceptType newAccept) {
// Determine if head
if (head == null) {
return newAccept; // only entry in list
}
// Determine if should be head
if (head.compare(newAccept) < 0) {
// Accept is to be new head
newAccept.next = head;
return newAccept;
}
// Insert somewhere in the list
AcceptType current = head;
while (current.next != null) {
// Determine if should come before next value
if (current.next.compare(newAccept) < 0) {
// Insert before next value
newAccept.next = current.next;
current.next = newAccept;
return head; // inserted
}
// Move to next position
current = current.next;
}
// As here, did not insert, so append to list
current.next = newAccept;
return head;
}
/**
* Abstract accept
content-type
value from the
* {@link HttpRequest}.
*/
private static abstract class AcceptType {
/**
* q
value. Used for sorting results.
*/
private String q;
/**
* Weight of wild card. Used for sorting results, with:
*
* 0
: * /*
* 1
: content\/*
* 2
: content/type
*
*/
private int wildcardWeight;
/**
* Number of parameters. Used for sorting results.
*/
private int parameterCount;
/**
* Next {@link AcceptType}.
*/
private AcceptType next = null;
/**
* Instantiate.
*
* @param q
* q
value.
* @param wildcardWeight
* Wild card weight.
* @param parameterCount
* Parameter count.
*/
protected AcceptType(String q, int wildcardWeight, int parameterCount) {
this.q = q;
this.wildcardWeight = wildcardWeight;
this.parameterCount = parameterCount;
}
/**
* Indicates if matches the {@link AcceptHandler}.
*
* @param acceptHandler
* {@link AcceptHandler}.
* @return true
if matches the {@link AcceptHandler}.
*/
protected abstract boolean isMatch(AcceptHandler acceptHandler);
/**
* Compares this against another {@link AcceptType}.
*
* @param other
* Other {@link AcceptType}.
* @return Compare -X / 0 / +X based on lesser, equal or greater matching
* weight.
*/
private int compare(AcceptType other) {
// Compare first on 'q' value
int compare = this.q.compareTo(other.q);
if (compare != 0) {
return compare;
}
// Next compare on wild card weight
compare = this.wildcardWeight - other.wildcardWeight;
if (compare != 0) {
return compare;
}
// Next compare on parameter count
compare = this.parameterCount - other.parameterCount;
if (compare != 0) {
return compare;
}
// As here, equal in sorting weight
return 0;
}
}
/**
* {@link AcceptType} for * /*.
*/
private static class AnyAcceptType extends AcceptType {
/**
* Instantiate.
*
* @param q
* q
value.
* @param parameterCount
* Parameter count.
*/
protected AnyAcceptType(String q, int parameterCount) {
super(q, 0, parameterCount);
}
/*
* =============== AcceptType ===============
*/
@Override
protected boolean isMatch(AcceptHandler acceptHandler) {
// Matches any content type
return true;
}
}
/**
* {@link AcceptType} for type/*.
*/
private static class TypeAcceptType extends AcceptType {
/**
* content-type
prefix.
*/
private final String contentPrefix;
/**
* Instantiate.
*
* @param contentPrefix
* content-type
prefix.
* @param q
* q
value.
* @param parameterCount
* Parameter count.
*/
protected TypeAcceptType(String contentPrefix, String q, int parameterCount) {
super(q, 1, parameterCount);
this.contentPrefix = contentPrefix;
}
/*
* =============== AcceptType ===============
*/
@Override
protected boolean isMatch(AcceptHandler acceptHandler) {
switch (acceptHandler.type) {
case SUB_TYPE:
return acceptHandler.matchContentType.startsWith(this.contentPrefix);
case TYPE:
return acceptHandler.matchContentType.equals(this.contentPrefix);
case ANY:
return true;
default:
throw new IllegalStateException(
"Unknown " + AcceptHandlerEnum.class.getName() + " type " + acceptHandler.type);
}
}
}
/**
* {@link AcceptType} for type/sub-type.
*/
private static class SubTypeAcceptType extends AcceptType {
/**
* content-type
.
*/
private final String contentType;
/**
* Instantiate.
*
* @param contentType
* content-type
.
* @param q
* q
value.
* @param parameterCount
* Parameter count.
*/
protected SubTypeAcceptType(String contentType, String q, int parameterCount) {
super(q, 2, parameterCount);
this.contentType = contentType;
}
/*
* =============== AcceptType ===============
*/
@Override
protected boolean isMatch(AcceptHandler acceptHandler) {
switch (acceptHandler.type) {
case SUB_TYPE:
return this.contentType.equals(acceptHandler.matchContentType);
case TYPE:
return this.contentType.startsWith(acceptHandler.matchContentType);
case ANY:
return true;
default:
throw new IllegalStateException(
"Unknown " + AcceptHandlerEnum.class.getName() + " type " + acceptHandler.type);
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy