org.xins.server.CallingConvention Maven / Gradle / Ivy
/*
* $Id: CallingConvention.java,v 1.116 2012/02/28 18:10:54 agoubard Exp $
*
* See the COPYRIGHT file for redistribution and use restrictions.
*/
package org.xins.server;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.xins.common.MandatoryArgumentChecker;
import org.xins.common.Utils;
import org.xins.common.manageable.Manageable;
import org.xins.common.text.TextUtils;
import org.xins.common.xml.ElementFormatter;
/**
* Abstraction of a calling convention. A calling convention determines how an
* HTTP request is converted to a XINS function invocation request and how a
* XINS function result is converted back to an HTTP response.
*
* Thread safety
*
* Calling convention implementations must be thread-safe.
*
* @version $Revision: 1.116 $ $Date: 2012/02/28 18:10:54 $
* @author Anthony Goubard
* @author Ernst de Haan
*
* @see CallingConventionManager
*/
abstract class CallingConvention extends Manageable {
/**
* The default value of the "Server"
header sent with an HTTP
* response. The actual value is
* "XINS/Java Server Framework "
, followed by the version of
* the framework.
*
*
TODO: Move this constant and the associated functionality elsewhere,
* since it does not seem to belong in this class.
*/
private static final String SERVER_HEADER
= "XINS/Java Server Framework " + Library.getVersion();
/**
* The default set of supported HTTP methods.
*/
private static final String[] DEFAULT_SUPPORTED_METHODS =
new String[] { "HEAD", "GET", "POST" };
/**
* The key used in the HttpRequest attribute used to cache the parsed
* XML Element when the request is an XML request.
*/
private static final String CACHED_XML_ELEMENT_KEY = "CACHED_XML_ELEMENT_KEY";
/**
* The current API. The value is set after the construction of the calling
* convention.
*/
private API _api;
/**
* The convention name associated with this calling convention (e.g. _xins-std).
*/
private String _conventionName;
/**
* Constructs a new CallingConvention
. A
* CallingConvention
instance can only be generated by the
* XINS/Java Server Framework.
*/
protected CallingConvention() {
}
/**
* Determines the current API.
*
* @return
* the current {@link API}, never null
.
*
* @since XINS 1.5.0
*/
protected final API getAPI() {
return _api;
}
/**
* Sets the current API.
*
* @param api
* the current {@link API}, never null
.
*/
final void setAPI(API api) {
_api = api;
}
/**
* Gets the name of the convention associated with this CC.
*
* @return
* the name of this calling convention, never null
.
*
* @since XINS 2.1
*/
final String getConventionName() {
return _conventionName;
}
/**
* Sets the name of the convention associated with this CC.
*
* @param conventionName
* the calling convention name, never null
.
*
* @since XINS 2.1
*/
final void setConventionName(String conventionName) {
_conventionName = conventionName;
}
/**
* Determines which HTTP methods are supported for function invocations.
*
*
Each String
in the returned array must be one
* supported method.
*
*
The returned array must not be null
, it must only
* contain valid HTTP method names, so they may not contain whitespace, for
* example. Duplicates will be ignored. HTTP method names must be in uppercase.
*
*
There must be at least one HTTP method supported for function
* invocations.
*
*
Note that OPTIONS must not be returned by this method, as it
* is not an HTTP method that can ever be used to invoke a XINS function.
*
HTTP OPTIONS requests are treated differently. For the path
* *
the capabilities of the whole server are returned. For other
* paths, the appropriate calling convention is determined, after which the
* set of supported HTTP methods is returned to the called.
*
* @return
* the HTTP methods supported, in a String
array, must
* not be null
.
*
* @since XINS 1.5.0
*/
protected String[] getSupportedMethods() {
return DEFAULT_SUPPORTED_METHODS;
}
/**
* Determines which HTTP methods are supported for function invocations,
* for the specified request.
*
*
Each String
in the returned array must be one
* supported method.
*
*
The returned array may be null
. If it is not, then the
* returned array must only contain valid HTTP method names, so they may
* not contain whitespace, for example. HTTP method names must be in uppercase.
*
*
There must be at least one HTTP method supported for function
* invocations.
*
*
Note that OPTIONS must not be returned by this method, as it
* is not an HTTP method that can ever be used to invoke a XINS function.
*
*
The set of supported methods must be a subset of the set returned by
* {@link #getSupportedMethods()}.
*
*
The default implementation of this method returns the set returned by
* {@link #getSupportedMethods()}.
*
* @param request
* the request to determine the supported methods for.
*
* @return
* the HTTP methods supported for the specified request, in a
* String
array, can be null
.
*
* @since XINS 1.5.0
*/
protected String[] getSupportedMethods(HttpServletRequest request) {
return getSupportedMethods();
}
/**
* Checks if the specified request can be handled by this calling
* convention. Assuming this CallingConvention
instance is
* usable and the HTTP method is supported, this method delegates to
* {@link #matches(HttpServletRequest)}.
*
*
If this calling convention is not usable (see {@link #isUsable()}),
* then false
is returned, even before calling
* {@link #matches(HttpServletRequest)}.
*
*
If this method does not support the HTTP method for function
* invocations, then false
is returned.
*
*
If {@link #matches(HttpServletRequest)} throws an exception, then
* this exception is ignored and false
is returned.
*
*
This method is guaranteed not to throw any exception.
*
* @param httpRequest
* the HTTP request to investigate, cannot be null
.
*
* @return
* true
if this calling convention is possibly
* able to handle this request, or false
if it is
* definitely not able to handle this request.
*/
final boolean matchesRequest(HttpServletRequest httpRequest) {
// First check if this CallingConvention instance is bootstrapped and
// initialized
if (! isUsable()) {
return false;
}
// Make sure the HTTP method is supported
String method = httpRequest.getMethod();
if (!Arrays.asList(getSupportedMethods(httpRequest)).contains(method) && !"OPTIONS".equals(method)) {
return false;
}
// Delegate to the 'matches' method
try {
return matches(httpRequest);
// Assume that an exception indicates the request cannot be handled
//
// NOTE: We do not log this exception, because it would possibly show up
// in the logs on a regular basis, drawing attention to a
// non-issue.
} catch (Throwable exception) {
return false;
}
}
/**
* Checks if the specified request can possibly be handled by this calling
* convention as a function invocation.
*
*
Implementations of this method should be optimized for performance,
* as this method may be called for each incoming request. Also, this
* method should not have any side-effects except possibly some caching in
* case there is a match.
*
*
If this method throws any exception, the exception is logged as an
* ignorable exception and false
is assumed.
*
*
This method should only be called by the XINS/Java Server Framework.
*
* @param httpRequest
* the HTTP request to investigate, never null
.
*
* @return
* true
if this calling convention is possibly
* able to handle this request, or false
if it is
* definitely not able to handle this request.
*
* @throws Exception
* if analysis of the request causes an exception; in this case
* false
will be assumed by the framework.
*
* @since XINS 1.4.0
*/
protected abstract boolean matches(HttpServletRequest httpRequest)
throws Exception;
/**
* Converts an HTTP request to a XINS request (wrapper method). This method
* checks the arguments, checks that the HTTP method is actually supported,
* calls the implementation method and then checks the return value from
* that method.
*
* @param httpRequest
* the HTTP request, cannot be null
.
*
* @return
* the XINS request object, never null
.
*
* @throws IllegalStateException
* if this calling convention is currently not usable, see
* {@link Manageable#assertUsable()}.
*
* @throws IllegalArgumentException
* if httpRequest == null
.
*
* @throws InvalidRequestException
* if the request is considerd to be invalid, at least for this calling
* convention; either because the HTTP method is not supported, or
* because {@link #convertRequestImpl(HttpServletRequest)} indicates so.
*
* @throws FunctionNotSpecifiedException
* if the request does not indicate the name of the function to execute.
*/
final FunctionRequest convertRequest(HttpServletRequest httpRequest)
throws IllegalStateException,
IllegalArgumentException,
InvalidRequestException,
FunctionNotSpecifiedException {
// Make sure the current state is okay
assertUsable();
// Check preconditions
MandatoryArgumentChecker.check("httpRequest", httpRequest);
// Delegate to the implementation method
FunctionRequest xinsRequest;
try {
xinsRequest = convertRequestImpl(httpRequest);
// Filter any thrown exceptions
} catch (Throwable exception) {
if (exception instanceof InvalidRequestException) {
throw (InvalidRequestException) exception;
} else if (exception instanceof FunctionNotSpecifiedException) {
throw (FunctionNotSpecifiedException) exception;
} else {
throw Utils.logProgrammingError(exception);
}
}
// Make sure the returned value is not null
if (xinsRequest == null) {
throw Utils.logProgrammingError("Method returned null.");
}
return xinsRequest;
}
/**
* Converts an HTTP request to a XINS request (implementation method). This
* method should only be called from the XINS/Java Server Framework self.
* Then it is guaranteed that:
*
* - the state is usable;
*
- the
httpRequest
argument is not null
;
* - the HTTP method is in the set of supported methods, as indicated
* by {@link #getSupportedMethods()}.
*
*
* Note that {@link #getSupportedMethods(HttpServletRequest)} will not
* have been called prior to this method call.
*
* @param httpRequest
* the HTTP request.
*
* @return
* the XINS request object, should not be null
.
*
* @throws InvalidRequestException
* if the request is considerd to be invalid.
*
* @throws FunctionNotSpecifiedException
* if the request does not indicate the name of the function to execute.
*/
protected abstract FunctionRequest convertRequestImpl(HttpServletRequest httpRequest)
throws InvalidRequestException,
FunctionNotSpecifiedException;
/**
* Converts a XINS result to an HTTP response (wrapper method). This method
* checks the arguments, then calls the implementation method and then
* checks the return value from that method.
*
*
Note that this method is not called if there is an error while
* converting the request.
*
* @param xinsResult
* the XINS result object that should be converted to an HTTP response,
* cannot be null
.
*
* @param httpResponse
* the HTTP response object to configure, cannot be null
.
*
* @param backpack
* the backpack, cannot be null
.
*
* @throws IllegalStateException
* if this calling convention is currently not usable, see
* {@link Manageable#assertUsable()}.
*
* @throws IllegalArgumentException
* if xinsResult == null
* || httpResponse == null
* || httpRequest == null
.
*
* @throws IOException
* if the invocation of any of the methods in either
* httpResponse
or httpRequest
caused an I/O
* error.
*/
final void convertResult(FunctionResult xinsResult,
HttpServletResponse httpResponse,
Map backpack)
throws IllegalStateException,
IllegalArgumentException,
IOException {
// Make sure the current state is okay
assertUsable();
// Check preconditions
MandatoryArgumentChecker.check("xinsResult", xinsResult,
"httpResponse", httpResponse,
"backpack", backpack);
// By default, all calling conventions return the same "Server" header.
// This can be overridden in the convertResultImpl() method.
httpResponse.addHeader("Server", SERVER_HEADER);
// Delegate to the implementation method
try {
convertResultImpl(xinsResult, httpResponse, backpack);
// Filter any thrown exceptions
} catch (Throwable exception) {
if (exception instanceof IOException) {
Log.log_3506(exception, getClass().getName());
throw (IOException) exception;
} else {
throw Utils.logProgrammingError(exception);
}
}
}
/**
* Converts a XINS result to an HTTP response (implementation method). This
* method should only be called from the XINS/Java Server Framework self.
* Then it is guaranteed that none of the arguments is null
.
*
* @param xinsResult
* the XINS result object that should be converted to an HTTP response,
* will not be null
.
*
* @param httpResponse
* the HTTP response object to configure.
*
* @param backpack
* the backpack.
*
* @throws IOException
* if the invocation of any of the methods in either
* httpResponse
or httpRequest
caused an I/O
* error.
*/
protected abstract void convertResultImpl(FunctionResult xinsResult,
HttpServletResponse httpResponse,
Map backpack)
throws IOException;
// XXX: Replace IOException with more appropriate exception?
/**
* Parses XML from the specified HTTP request and checks that the content
* type is correct.
*
* This method uses a cache to optimize performance if either of the
* parseXMLRequest
methods is called multiple times for the
* same request.
*
*
Calling this method is equivalent with calling
* {@link #parseXMLRequest(HttpServletRequest,boolean)} with the
* checkType
argument set to true
.
*
* @param httpRequest
* the HTTP request, cannot be null
.
*
* @return
* the parsed element, never null
.
*
* @throws IllegalArgumentException
* if httpRequest == null
.
*
* @throws InvalidRequestException
* if the HTTP request cannot be read or cannot be parsed correctly.
*
* @since XINS 1.4.0
*/
protected Element parseXMLRequest(HttpServletRequest httpRequest)
throws IllegalArgumentException, InvalidRequestException {
return parseXMLRequest(httpRequest, true);
}
/**
* Parses XML from the specified HTTP request and optionally checks that
* the content type is correct.
*
*
Since XINS 1.4.0, this method uses a cache to optimize performance if
* either of the parseXMLRequest
methods is called multiple
* times for the same request.
*
* @param httpRequest
* the HTTP request, cannot be null
.
*
* @param checkType
* flag indicating whether this method should check that the content
* type of the request is text/xml.
*
* @return
* the parsed element, never null
.
*
* @throws IllegalArgumentException
* if httpRequest == null
.
*
* @throws InvalidRequestException
* if the HTTP request cannot be read or cannot be parsed correctly.
*
* @since XINS 1.3.0
*/
protected Element parseXMLRequest(HttpServletRequest httpRequest,
boolean checkType)
throws IllegalArgumentException, InvalidRequestException {
// Check arguments
MandatoryArgumentChecker.check("httpRequest", httpRequest);
// Determine if the request matches the cached request and the parsed
// XML is already cached
Object cached = httpRequest.getAttribute(CACHED_XML_ELEMENT_KEY);
// Cache miss
if (cached == null) {
Log.log_3512();
// Cache hit
} else {
Log.log_3513();
return (Element) cached;
}
// Always first check the content type, even if checking is enabled. We
// do this because the parsed request will only be stored if the content
// type was OK.
String contentType = httpRequest.getContentType();
String errorMessage = null;
if (contentType == null || contentType.trim().length() < 1) {
errorMessage = "No content type set.";
} else {
String contentTypeLC = contentType.toLowerCase();
if (! ("text/xml".equals(contentTypeLC) ||
contentTypeLC.startsWith("text/xml;"))) {
errorMessage = "Invalid content type \""
+ contentType
+ "\". Expected \"text/xml\" (case-insensitive) or a variant of it.";
}
}
// The content-type check was unsuccessful
if (errorMessage != null) {
// Log: Not caching XML since the content type is not "text/xml"
Log.log_3515();
// If checking is enabled
if (checkType) {
throw new InvalidRequestException(errorMessage);
}
}
// Parse the content in the HTTP request
Element element;
try {
element = ElementFormatter.parse(httpRequest.getReader());
// I/O error
} catch (IOException ex) {
String message = "Failed to read XML request.";
throw new InvalidRequestException(message, ex);
// Parsing error
} catch (SAXException ex) {
String message = "Failed to parse XML request.";
throw new InvalidRequestException(message, ex);
}
// Only store in the cache if the content type was OK
if (errorMessage == null) {
httpRequest.setAttribute(CACHED_XML_ELEMENT_KEY, element);
Log.log_3514();
}
return element;
}
/**
* Gathers all parameters from the specified request. The parameters are
* returned as a {@link Map}.
* If no parameters are found, then null
is returned.
*
*
If a parameter is found to have multiple values, then an
* {@link InvalidRequestException} is thrown.
*
* @param httpRequest
* the HTTP request to get the parameters from, cannot be
* null
.
*
* @return
* the properties found, or null
if none were found.
*
* @throws InvalidRequestException
* if a parameter is found that has multiple values.
*/
Map gatherParams(HttpServletRequest httpRequest) throws InvalidRequestException {
// Get the parameters from the HTTP request
Enumeration params = httpRequest.getParameterNames();
// The property set to return from this method
Map pr;
// If there are no parameters, then return null
if (! params.hasMoreElements()) {
pr = null;
// There seem to be some parameters
} else {
pr = new HashMap();
do {
// Get the parameter name
String name = (String) params.nextElement();
// Get all parameter values (can be multiple)
String[] values = httpRequest.getParameterValues(name);
// Be gentle, allow nulls and zero-sized arrays
if (values != null && values.length != 0) {
// Get the parameter value, allowing duplicate values, but not
// different ones; this may throw an InvalidRequestException
String value = getParamValue(name, values);
// Associate the name with the one and only value
pr.put(name, value);
}
} while (params.hasMoreElements());
}
return pr;
}
/**
* Changes a parameter set to remove all parameters that should not be
* passed to functions.
*
* A parameter will be removed if it matches any of the following
* conditions:
*
*
* - parameter name is
null
;
* - parameter name is empty;
*
- parameter value is
null
;
* - parameter value is empty;
*
- parameter name equals
"function"
.
*
*
* @param parameters
* the {@link Map} containing the set of parameters
* to investigate, cannot be null
.
*
* @throws IllegalArgumentException
* if parameters == null
.
*/
static void cleanUpParameters(Map parameters)
throws IllegalArgumentException {
// Check arguments
MandatoryArgumentChecker.check("parameters", parameters);
// Loop through all parameters
List toRemove = new ArrayList();
for (Map.Entry parameter : parameters.entrySet()) {
// Determine parameter name and value
String name = parameter.getKey();
String value = parameter.getValue();
// If the parameter name or value is empty, or if the name is
// "function", then mark the parameter as 'to be removed'.
// Parameters starting with an underscore are reserved for XINS, so
// mark these as 'to be removed' as well.
if (TextUtils.isEmpty(name) || TextUtils.isEmpty(value) || "function".equals(name) || name.charAt(0) == '_') {
toRemove.add(name);
}
}
// If there is anything to remove, then do so
for (String name : toRemove) {
parameters.remove(name);
}
}
/**
* Determines a single value for a parameter based on an array of values.
* If there is only one value, then that value is returned. If there are
* multiple equal values, then the value is returned as well. However, if
* there are multiple values and at least one of them is different, then an
* {@link InvalidRequestException} is thrown.
*
* @param name
* the name of the parameter, only used when throwing an
* {@link InvalidRequestException}, should not be null
.
*
* @param values
* the values, should not be null
and should not have a
* size of zero.
*
* @return
* the single value of the parameter, if any.
*
* @throws NullPointerException
* if values == null || values[n] == null
, where
* 0 <= n < values.length
.
*
* @throws IndexOutOfBoundsException
* if values.length < 1
.
*
* @throws InvalidRequestException
* if the parameter is found to have multiple different values.
*/
private final String getParamValue(String name, String[] values)
throws NullPointerException,
IndexOutOfBoundsException,
InvalidRequestException {
String value = values[0];
// We only need to do crunching if there is more than one value
if (values.length > 1) {
for (int i = 1; i < values.length; i++) {
String other = values[i];
if (! value.equals(other)) {
throw new InvalidRequestException("Found multiple values for the parameter named \"" + name + "\".");
}
}
}
return value;
}
}