| CallingConvention.java |
/*
* $Id: CallingConvention.java,v 1.121 2013/01/22 15:13:22 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.spec.EntityNotFoundException;
import org.xins.common.spec.InvalidSpecificationException;
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.
*
* <h2>Thread safety</h2>
*
* <p>Calling convention implementations must be thread-safe.
*
* @version $Revision: 1.121 $ $Date: 2013/01/22 15:13:22 $
* @author <a href="mailto:anthony.goubard@japplis.com">Anthony Goubard</a>
* @author <a href="mailto:ernst@ernstdehaan.com">Ernst de Haan</a>
*
* @see CallingConventionManager
*/
abstract class CallingConvention extends Manageable {
/**
* The default value of the <code>"Server"</code> header sent with an HTTP
* response. The actual value is
* <code>"XINS/Java Server Framework "</code>, followed by the version of
* the framework.
*
* <p>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 <code>CallingConvention</code>. A
* <code>CallingConvention</code> instance can only be generated by the
* XINS/Java Server Framework.
*/
protected CallingConvention() {
}
/**
* Determines the current API.
*
* @return
* the current {@link API}, never <code>null</code>.
*
* @since XINS 1.5.0
*/
protected final API getAPI() {
return _api;
}
/**
* Sets the current API.
*
* @param api
* the current {@link API}, never <code>null</code>.
*/
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 <code>null</code>.
*
* @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 <code>null</code>.
*
* @since XINS 2.1
*/
final void setConventionName(String conventionName) {
_conventionName = conventionName;
}
/**
* Determines which HTTP methods are supported for function invocations.
*
* <p>Each <code>String</code> in the returned array must be one
* supported method.
*
* <p>The returned array must not be <code>null</code>, 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.
*
* <p>There must be at least one HTTP method supported for function
* invocations.
*
* <p>Note that <em>OPTIONS</em> must not be returned by this method, as it
* is not an HTTP method that can ever be used to invoke a XINS function.
* <p>HTTP <em>OPTIONS</em> requests are treated differently. For the path
* <code>*</code> 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 <code>String</code> array, must
* not be <code>null</code>.
*
* @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.
*
* <p>Each <code>String</code> in the returned array must be one
* supported method.
*
* <p>The returned array may be <code>null</code>. 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.
*
* <p>There must be at least one HTTP method supported for function
* invocations.
*
* <p>Note that <em>OPTIONS</em> must not be returned by this method, as it
* is not an HTTP method that can ever be used to invoke a XINS function.
*
* <p>The set of supported methods must be a subset of the set returned by
* {@link #getSupportedMethods()}.
*
* <p>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
* <code>String</code> array, can be <code>null</code>.
*
* @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 <code>CallingConvention</code> instance is
* usable and the HTTP method is supported, this method delegates to
* {@link #matches(HttpServletRequest)}.
*
* <p>If this calling convention is not usable (see {@link #isUsable()}),
* then <code>false</code> is returned, even <em>before</em> calling
* {@link #matches(HttpServletRequest)}.
*
* <p>If this method does not support the HTTP method for function
* invocations, then <code>false</code> is returned.
*
* <p>If {@link #matches(HttpServletRequest)} throws an exception, then
* this exception is ignored and <code>false</code> is returned.
*
* <p>This method is guaranteed not to throw any exception.
*
* @param httpRequest
* the HTTP request to investigate, cannot be <code>null</code>.
*
* @return
* <code>true</code> if this calling convention is <em>possibly</em>
* able to handle this request, or <code>false</code> if it is
* <em>definitely</em> 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.
*
* <p>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.
*
* <p>If this method throws any exception, the exception is logged as an
* ignorable exception and <code>false</code> is assumed.
*
* <p>This method should only be called by the XINS/Java Server Framework.
*
* @param httpRequest
* the HTTP request to investigate, never <code>null</code>.
*
* @return
* <code>true</code> if this calling convention is <em>possibly</em>
* able to handle this request, or <code>false</code> if it is
* <em>definitely</em> not able to handle this request.
*
* @throws Exception
* if analysis of the request causes an exception; in this case
* <code>false</code> 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 <code>null</code>.
*
* @return
* the XINS request object, never <code>null</code>.
*
* @throws IllegalStateException
* if this calling convention is currently not usable, see
* {@link Manageable#assertUsable()}.
*
* @throws IllegalArgumentException
* if <code>httpRequest == null</code>.
*
* @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 {
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:
* <ul>
* <li>the state is usable;
* <li>the <code>httpRequest</code> argument is not <code>null</code>;
* <li>the HTTP method is in the set of supported methods, as indicated
* by {@link #getSupportedMethods()}.
* </ul>
*
* <p>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 <code>null</code>.
*
* @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.
*
* <p>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 <code>null</code>.
*
* @param httpResponse
* the HTTP response object to configure, cannot be <code>null</code>.
*
* @param backpack
* the backpack, cannot be <code>null</code>.
*
* @throws IllegalStateException
* if this calling convention is currently not usable, see
* {@link Manageable#assertUsable()}.
*
* @throws IllegalArgumentException
* if <code>xinsResult == null
* || httpResponse == null
* || httpRequest == null</code>.
*
* @throws IOException
* if the invocation of any of the methods in either
* <code>httpResponse</code> or <code>httpRequest</code> caused an I/O
* error.
*/
final void convertResult(FunctionResult xinsResult,
HttpServletResponse httpResponse,
Map<String, Object> 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);
// Send HTTP 304 if not modified
if (xinsResult instanceof NotModifiedResult) {
httpResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return;
}
// Set the Cache-Control header if specified
addCacheHeader(xinsResult, httpResponse, backpack);
// 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);
}
}
}
/**
* Adds the cache control header if the method could be cached.
*
* @param xinsResult
* the XINS result object that should be converted to an HTTP response,
* cannot be <code>null</code>.
*
* @param httpResponse
* the HTTP response object to configure, cannot be <code>null</code>.
*
* @param backpack
* the backpack, cannot be <code>null</code>.
*/
private void addCacheHeader(FunctionResult xinsResult, HttpServletResponse httpResponse, Map<String, Object> backpack) {
Integer cacheInSeconds = (Integer) backpack.get(BackpackConstants.CACHE);
if (cacheInSeconds == null && xinsResult.getErrorCode() == null) {
String functionName = (String) backpack.get(BackpackConstants.FUNCTION_NAME);
if (!functionName.startsWith("_")) {
try {
cacheInSeconds = _api.getAPISpecification().getFunction(functionName).getCache();
} catch (InvalidSpecificationException ise) {
// keep the old value
} catch (EntityNotFoundException enfe) {
// keep the old value
}
}
}
if (cacheInSeconds != null && cacheInSeconds > 0) {
httpResponse.addHeader("Cache-Control", "max-age=" + cacheInSeconds);
}
}
/**
* 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 <code>null</code>.
*
* @param xinsResult
* the XINS result object that should be converted to an HTTP response,
* will not be <code>null</code>.
*
* @param httpResponse
* the HTTP response object to configure.
*
* @param backpack
* the backpack.
*
* @throws IOException
* if the invocation of any of the methods in either
* <code>httpResponse</code> or <code>httpRequest</code> caused an I/O
* error.
*/
protected abstract void convertResultImpl(FunctionResult xinsResult,
HttpServletResponse httpResponse,
Map<String, Object> 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.
*
* <p>This method uses a cache to optimize performance if either of the
* <code>parseXMLRequest</code> methods is called multiple times for the
* same request.
*
* <p>Calling this method is equivalent with calling
* {@link #parseXMLRequest(HttpServletRequest,boolean)} with the
* <code>checkType</code> argument set to <code>true</code>.
*
* @param httpRequest
* the HTTP request, cannot be <code>null</code>.
*
* @return
* the parsed element, never <code>null</code>.
*
* @throws IllegalArgumentException
* if <code>httpRequest == null</code>.
*
* @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.
*
* <p>Since XINS 1.4.0, this method uses a cache to optimize performance if
* either of the <code>parseXMLRequest</code> methods is called multiple
* times for the same request.
*
* @param httpRequest
* the HTTP request, cannot be <code>null</code>.
*
* @param checkType
* flag indicating whether this method should check that the content
* type of the request is <em>text/xml</em>.
*
* @return
* the parsed element, never <code>null</code>.
*
* @throws IllegalArgumentException
* if <code>httpRequest == null</code>.
*
* @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 <code>null</code> is returned.
*
* <p>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
* <code>null</code>.
*
* @return
* the properties found, or <code>null</code> if none were found.
*
* @throws InvalidRequestException
* if a parameter is found that has multiple values.
*/
Map<String, String> 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<String, String> pr;
// If there are no parameters, then return null
if (! params.hasMoreElements()) {
pr = null;
// There seem to be some parameters
} else {
pr = new HashMap<String, String>();
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.
*
* <p>A parameter will be removed if it matches any of the following
* conditions:
*
* <ul>
* <li>parameter name is <code>null</code>;
* <li>parameter name is empty;
* <li>parameter value is <code>null</code>;
* <li>parameter value is empty;
* <li>parameter name equals <code>"function"</code>.
* </ul>
*
* @param parameters
* the {@link Map} containing the set of parameters
* to investigate, cannot be <code>null</code>.
*
* @throws IllegalArgumentException
* if <code>parameters == null</code>.
*/
static void cleanUpParameters(Map<String, String> parameters)
throws IllegalArgumentException {
// Check arguments
MandatoryArgumentChecker.check("parameters", parameters);
// Loop through all parameters
List<String> toRemove = new ArrayList();
for (Map.Entry<String, String> 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 <code>null</code>.
*
* @param values
* the values, should not be <code>null</code> and should not have a
* size of zero.
*
* @return
* the single value of the parameter, if any.
*
* @throws NullPointerException
* if <code>values == null || values[<em>n</em>] == null</code>, where
* <code>0 <= <em>n</em> < values.length</code>.
*
* @throws IndexOutOfBoundsException
* if <code>values.length < 1</code>.
*
* @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;
}
}