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

org.xins.server.CallingConvention Maven / Gradle / Ivy

The newest version!
/*
 * $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; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy