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; } }