All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.officefloor.web.accept.AcceptNegotiatorImpl Maven / Gradle / Ivy

There is a newer version: 3.40.0
Show newest version
/*
 * 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); } } } }