CallingConventionManager.java |
/* * $Id: CallingConventionManager.java,v 1.94 2013/01/23 09:59:40 agoubard Exp $ * * See the COPYRIGHT file for redistribution and use restrictions. */ package org.xins.server; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.http.HttpServletRequest; import org.xins.common.Utils; import org.xins.common.collections.InvalidPropertyValueException; import org.xins.common.collections.MissingRequiredPropertyException; import org.xins.common.manageable.BootstrapException; import org.xins.common.manageable.InitializationException; import org.xins.common.manageable.Manageable; import org.xins.common.text.TextUtils; /** * Manages the <code>CallingConvention</code> instances for the API. * * @version $Revision: 1.94 $ $Date: 2013/01/23 09:59:40 $ * @author <a href="mailto:mees.witteman@orange-ftgroup.com">Mees Witteman</a> * @author <a href="mailto:anthony.goubard@japplis.com">Anthony Goubard</a> * @author <a href="mailto:ernst@ernstdehaan.com">Ernst de Haan</a> * * @see CallingConvention */ class CallingConventionManager extends Manageable { /** * The name of the bootstrap property that specifies the name of the default * calling convention. */ private static final String API_CALLING_CONVENTION_PROPERTY = "org.xins.api.calling.convention"; /** * The name of the bootstrap property that specifies the class of the default * calling convention. */ private static final String API_CALLING_CONVENTION_CLASS_PROPERTY = "org.xins.api.calling.convention.class"; /** * The name of the request parameter that specifies the name of the calling * convention to use. */ static final String CALLING_CONVENTION_PARAMETER = "_convention"; /** * The name of the XINS standard calling convention. */ private static final String STANDARD_CALLING_CONVENTION = "_xins-std"; /** * The XINS XML calling convention. */ private static final String XML_CALLING_CONVENTION = "_xins-xml"; /** * The XINS XSLT calling convention. */ private static final String XSLT_CALLING_CONVENTION = "_xins-xslt"; /** * The name of the SOAP calling convention. * * @since XINS 1.3.0 */ private static final String SOAP_CALLING_CONVENTION = "_xins-soap"; /** * The name of the SOAP calling convention with mapping. * * @since XINS 2.1 */ private static final String SOAP_MAP_CALLING_CONVENTION = "_xins-soap-map"; /** * The name of the XML-RPC calling convention. * * @since XINS 1.3.0 */ private static final String XML_RPC_CALLING_CONVENTION = "_xins-xmlrpc"; /** * The name of the JSON-RPC calling convention. * * @since XINS 2.0 */ public static final String JSON_RPC_CALLING_CONVENTION = "_xins-jsonrpc"; /** * The name of the JSON-RPC 2 calling convention. * * @since XINS 3.1 */ public static final String JSON_RPC2_CALLING_CONVENTION = "_xins-jsonrpc2"; /** * The name of the JSON calling convention. The call is a Yahoo! style call. * * @since XINS 2.0 */ private static final String JSON_CALLING_CONVENTION = "_xins-json"; /** * List of the names of the calling conventions currently included in * XINS. */ private static final List<String> CONVENTIONS = Arrays.asList(new String[] { STANDARD_CALLING_CONVENTION, XML_CALLING_CONVENTION, XSLT_CALLING_CONVENTION, SOAP_CALLING_CONVENTION, SOAP_MAP_CALLING_CONVENTION, XML_RPC_CALLING_CONVENTION, JSON_RPC_CALLING_CONVENTION, JSON_RPC2_CALLING_CONVENTION, JSON_CALLING_CONVENTION }); /** * Array of type <code>Class</code> that is used when constructing a * <code>CallingConvention</code> instance via RMI. */ private static final Class[] CONSTRUCTOR_ARG_CLASSES = { API.class }; /** * Placeholder object used to indicate that the construction of a calling * convention object failed. Never <code>null</code>. */ private static final Object CREATION_FAILED = new Object(); /** * The API. Never <code>null</code>. */ private final API _api; /** * The name of the default calling convention. There is always a default * calling convention (at least after bootstrapping). * * <p>This field is initialized during bootstrapping. */ private String _defaultConventionName; /** * The names of the possible calling conventions. */ private List<String> _conventionNames; /** * Map containing all calling conventions. The key is the name of the * calling convention, the value is the calling convention object, or * {@link #CREATION_FAILED} if the calling convention object could not be * constructed. */ private final HashMap<String, Object> _conventions; /** * Creates a <code>CallingConventionManager</code> for the specified API. * * @param api * the API, cannot be <code>null</code>. */ CallingConventionManager(API api) { // Store the reference to the API _api = api; // Fill the list of the convention names with the pre defined conventions _conventionNames = new ArrayList<String>(); _conventionNames.addAll(CONVENTIONS); // Create a map to store the conventions in _conventions = new HashMap<String, Object>(12); } /** * Performs the bootstrap procedure (actual implementation). * * @param properties * the bootstrap properties, not <code>null</code>. * * @throws MissingRequiredPropertyException * if a required property is not given. * * @throws InvalidPropertyValueException * if the value of a certain property is invalid. * * @throws BootstrapException * if the bootstrapping failed for any other reason. */ protected void bootstrapImpl(Map<String, String> properties) throws MissingRequiredPropertyException, InvalidPropertyValueException, BootstrapException { // Determine the name and class of the custom calling convention _defaultConventionName = determineDefaultConvention(properties); // Append the defined calling conventions for (String property : properties.keySet()) { if (property.startsWith(API_CALLING_CONVENTION_PROPERTY + '.') && !property.equals(API_CALLING_CONVENTION_CLASS_PROPERTY)) { String conventionName = property.substring(32, property.length() - 6); _conventionNames.add(conventionName); } } // Construct and bootstrap the default calling convention CallingConvention cc = create(properties, _defaultConventionName); // If created, store the object and attempt bootstrapping if (cc != null) { _conventions.put(_defaultConventionName, cc); bootstrap(_defaultConventionName, cc, properties); if (cc.getState() != Manageable.BOOTSTRAPPED) { throw new BootstrapException("Failed to bootstrap the default calling convention."); } // Otherwise, if it's the default calling convention, fails } else { throw new BootstrapException("Failed to create the default calling convention."); } } /** * Determines the default calling convention. * * @param properties * the bootstrap properties, cannot be <code>null</code>. * * @return * the name of the default calling convention, never <code>null</code>. * * @throws MissingRequiredPropertyException * if a required property is not given. * * @throws InvalidPropertyValueException * if the value of a certain property is invalid. */ private String determineDefaultConvention(Map<String, String> properties) throws MissingRequiredPropertyException, InvalidPropertyValueException { // Name of the default calling convention (if any) String name = TextUtils.trim(properties.get(API_CALLING_CONVENTION_PROPERTY), null); // No calling convention defined if (name == null) { // Log: No custom calling convention specified Log.log_3246(); // Fallback to the XINS-specified default calling convention name = STANDARD_CALLING_CONVENTION; } // Log: Determined default calling convention Log.log_3245(name); // Return the name of the default calling convention return name; } /** * Constructs the calling convention with the specified name, using the * specified bootstrap properties. This method is called for both * <em>regular</em> and <em>custom</em> calling conventions. * * <p>If the name does not identify a recognized calling convention, then * <code>null</code> is returned. * * @param properties * the bootstrap properties, cannot be <code>null</code>. * * @param name * the name of the calling convention to construct, cannot be * <code>null</code>. * * @return * a non-bootstrapped {@link CallingConvention} instance that matches * the specified name, or <code>null</code> if no match is found. */ private CallingConvention create(Map<String, String> properties, String name) { // Determine the name of the CallingConvention class String className; if (name.charAt(0) == '_') { className = classNameForRegular(name); } else { className = properties.get(API_CALLING_CONVENTION_PROPERTY + '.' + name + ".class"); } // If the class could not be determined, then return null if (className == null) { Log.log_3239(null, name, null); return null; } Log.log_3237(name, className); // Construct a CallingConvention instance CallingConvention cc = construct(name, className); // NOTE: Logging of construction failures is done in construct(...) // Constructed successfully if (cc != null) { cc.setAPI(_api); cc.setConventionName(name); } return cc; } /** * Determines the name of the class that represents the regular calling * convention with the specified name. A <em>regular</em> calling * convention is one that comes with the XINS framework. * * @param name * the name of the calling convention, should not be <code>null</code> * and should normally starts with an underscore character * (<code>'_'</code>). * * @return * the name of the {@link CallingConvention} class that matches the * specified calling convention name, or <code>null</code> if unknown. */ private String classNameForRegular(String name) { // XINS standard if (name.equals(STANDARD_CALLING_CONVENTION)) { return "org.xins.server.StandardCallingConvention"; // XINS XML } else if (name.equals(XML_CALLING_CONVENTION)) { return "org.xins.server.XMLCallingConvention"; // XSLT } else if (name.equals(XSLT_CALLING_CONVENTION)) { return "org.xins.server.XSLTCallingConvention"; // SOAP } else if (name.equals(SOAP_CALLING_CONVENTION)) { return "org.xins.server.SOAPCallingConvention"; // SOAP MAP } else if (name.equals(SOAP_MAP_CALLING_CONVENTION)) { return "org.xins.server.SOAPMapCallingConvention"; // XML-RPC } else if (name.equals(XML_RPC_CALLING_CONVENTION)) { return "org.xins.server.XMLRPCCallingConvention"; // JSON-RPC } else if (name.equals(JSON_RPC_CALLING_CONVENTION)) { return "org.xins.server.JSONRPCCallingConvention"; // JSON-RPC2 } else if (name.equals(JSON_RPC2_CALLING_CONVENTION)) { return "org.xins.server.JSONRPC2CallingConvention"; // JSON } else if (name.equals(JSON_CALLING_CONVENTION)) { return "org.xins.server.JSONCallingConvention"; // Unrecognized } else { return null; } } /** * Constructs a new <code>CallingConvention</code> instance by class name. * * @param name * the name of the calling convention, cannot be <code>null</code>. * * @param className * the name of the class, cannot be <code>null</code>. * * @return * the constructed {@link CallingConvention} instance, or * <code>null</code> if the construction failed. */ private CallingConvention construct(String name, String className) { // Try to load the class Class clazz; try { clazz = Class.forName(className, true, Utils.getContextClassLoader()); } catch (Throwable exception) { Log.log_3239(exception, name, className); return null; } // Get the constructor that accepts an API argument Constructor con = null; try { con = clazz.getConstructor(CONSTRUCTOR_ARG_CLASSES); } catch (NoSuchMethodException exception) { // fall through, do not even log } // If there is such a constructor, invoke it if (con != null) { // Invoke it Object[] args = { _api }; try { CallingConvention cc = (CallingConvention) con.newInstance(args); return cc; // If the constructor exists but failed, then construction failed } catch (Throwable exception) { Utils.logIgnoredException(exception); return null; } } // Secondly try a constructor with no arguments try { CallingConvention cc = (CallingConvention) clazz.newInstance(); return cc; } catch (Throwable exception) { Log.log_3239(exception, name, className); return null; } } /** * Bootstraps the specified calling convention. * * @param name * the name of the calling convention, cannot be <code>null</code>. * * @param cc * the {@link CallingConvention} object to bootstrap, cannot be * <code>null</code>. * * @param properties * the bootstrap properties, cannot be <code>null</code>. */ private void bootstrap(String name, CallingConvention cc, Map<String, String> properties) { // Bootstrapping calling convention Log.log_3240(name); try { cc.bootstrap(properties); // Missing property } catch (MissingRequiredPropertyException exception) { Log.log_3242(name, exception.getPropertyName(), exception.getDetail()); // Invalid property } catch (InvalidPropertyValueException exception) { Log.log_3243(name, exception.getPropertyName(), exception.getPropertyValue(), exception.getReason()); // Catch BootstrapException and any other exceptions not caught // by previous catch statements } catch (Throwable exception) { Log.log_3244(exception, name); } } /** * Performs the initialization procedure (actual implementation). * * @param properties * the initialization properties, not null. * * @throws MissingRequiredPropertyException * if a required property is not given. * * @throws InvalidPropertyValueException * if the value of a certain property is invalid. * * @throws InitializationException * if the initialization failed, for any other reason. */ @Override protected void initImpl(Map<String, String> properties) throws MissingRequiredPropertyException, InvalidPropertyValueException, InitializationException { // Loop through all CallingConvention instances for (Map.Entry entry : _conventions.entrySet()) { // Determine the name and get the CallingConvention instance String name = (String) entry.getKey(); Object cc = entry.getValue(); // Process this CallingConvention only if it was created OK if (cc != CREATION_FAILED) { // Initialize the CallingConvention CallingConvention conv = (CallingConvention) cc; init(name, conv, properties); // Fail if the *default* calling convention fails to initialize if (!conv.isUsable() && name.equals(_defaultConventionName)) { throw new InitializationException("Failed to initialize the default calling convention \"" + name + "\"."); } } } } /** * Initializes the specified calling convention. * * <p>If the specified calling convention is not even bootstrapped, the * initialization is not even attempted. * * @param name * the name of the calling convention, cannot be <code>null</code>. * * @param cc * the {@link CallingConvention} object to initialize, cannot be * <code>null</code>. * * @param properties * the initialization properties, cannot be <code>null</code>. */ private void init(String name, CallingConvention cc, Map<String, String> properties) { // If the CallingConvention is not even bootstrapped, then do not even // attempt to initialize it if (! cc.isBootstrapped()) { return; } // Initialize calling convention Log.log_3435(name); try { cc.init(properties); // Missing property } catch (MissingRequiredPropertyException exception) { Log.log_3437(name, exception.getPropertyName(), exception.getDetail()); // Invalid property } catch (InvalidPropertyValueException exception) { Log.log_3438(name, exception.getPropertyName(), exception.getPropertyValue(), exception.getReason()); // Catch InitializationException and any other exceptions not caught // by previous catch statements } catch (Throwable exception) { Log.log_3439(exception, name); } } /** * Determines the calling convention to use for the specified request. * * @param request * the incoming request, cannot be <code>null</code>. * * @return * the calling convention to use, never <code>null</code>. * * @throws InvalidRequestException * if the request is considered invalid, for example because the calling * convention specified in the request is unknown. */ CallingConvention getCallingConvention(HttpServletRequest request) throws InvalidRequestException { // Get the value of the input parameter that determines the convention String ccName = request.getParameter(CALLING_CONVENTION_PARAMETER); // If a calling convention is specified then use that one if (! TextUtils.isEmpty(ccName)) { CallingConvention cc = getCallingConvention(ccName); if (! Arrays.asList(cc.getSupportedMethods(request)).contains(request.getMethod()) && !"OPTIONS".equals(request.getMethod())) { String detail = "Calling convention \"" + ccName + "\" does not support the \"" + request.getMethod() + "\" for this request."; Log.log_3507(ccName, detail); throw new InvalidRequestException(detail); } return cc; // Otherwise try to detect which one is appropriate } else { return detectCallingConvention(request); } } /** * Gets the calling convention for the given name. * * <p>The returned calling convention is bootstrapped and initialized. * * @param name * the name of the calling convention to retrieve, should not be * <code>null</code>. * * @return * the calling convention initialized, never <code>null</code>. * * @throws InvalidRequestException * if the calling convention name is unknown. */ private CallingConvention getCallingConvention(String name) throws InvalidRequestException { // Get the CallingConvention object Object o = _conventions.get(name); // Not found if (o == null && !_conventionNames.contains(name)) { String detail = "Calling convention \"" + name + "\" is unknown."; Log.log_3507(name, detail); throw new InvalidRequestException(detail); } else if (o == null) { // Create the asked calling convention and initiaze it CallingConvention cc = create(_api.getBootstrapProperties(), name); // If created, store the object and attempt bootstrapping if (cc != null) { o = cc; _conventions.put(name, cc); bootstrap(name, cc, _api.getBootstrapProperties()); init(name, cc, _api.getRuntimeProperties()); } else { o = CREATION_FAILED; _conventions.put(name, o); } } // Creation failed if (o == CREATION_FAILED) { String detail = "Calling convention \"" + name + "\" is known, but could not be created."; Log.log_3507(name, detail); throw new InvalidRequestException(detail); // Calling convention is recognized and was created OK } else { // Not usable (so not bootstrapped and initialized) CallingConvention cc = (CallingConvention) o; if (! cc.isUsable()) { String detail = "Calling convention \"" + name + "\" is known, but is uninitialized."; Log.log_3507(name, detail); throw new InvalidRequestException(detail); } return cc; } } /** * Gets the calling convention for the given name, or <code>null</code> if * the calling convention is not found or not usable. * * <p>The returned calling convention is bootstrapped and initialized. * * @param name * the name of the calling convention to retrieve, should not be * <code>null</code>. * * @return * the calling convention, or <code>null</code>. */ CallingConvention getCallingConvention2(String name) { try { return getCallingConvention(name); } catch (InvalidRequestException ex) { return null; } } /** * Attempts to detect which calling convention is the most appropriate for * an incoming request. This method is called when the calling convention * is not explicitly specified in the request. * * <p>The {@link CallingConvention#matchesRequest(HttpServletRequest)} * method is used to determine which calling conventions match. Then * the following algorithm is used to chose one: * * <ul> * <li>if the default calling convention matches, use that; * <li>otherwise if the {@link XSLTCallingConvention} matches and at * least one of the parameters specific for the this calling * convention is set, then use it; * <li>otherwise if the {@link StandardCallingConvention} matches, use * that; * <li>otherwise if there is exactly one other calling convention that * matches, use that one; * <li>if none of the calling conventions match, throw an * {@link InvalidRequestException}, indicating that no match could * be found; * <li>if multiple calling conventions match, throw an * {@link InvalidRequestException}, indicating that several matches * were found; * </ul> * * @param request * the incoming request, cannot be <code>null</code>. * * @return * the calling convention to use, never <code>null</code>. * * @throws InvalidRequestException * if the request is considered invalid, for example because the calling * convention specified in the request is unknown. */ CallingConvention detectCallingConvention(HttpServletRequest request) throws InvalidRequestException { // Log: Request does not specify any calling convention Log.log_3508(); // See if the default calling convention matches CallingConvention defCC = getCallingConvention2(_defaultConventionName); if (defCC != null && defCC.matchesRequest(request)) { Log.log_3509(defCC.getClass().getName()); return defCC; } // If not, see if XSLT-specific properties are set /and/ _xins-xslt matches CallingConvention xslCC = getCallingConvention2("_xins-xslt"); if (xslCC != null && xslCC != defCC && xslCC.matchesRequest(request)) { // Determine if one of the two XSLT-specific parameters is set String p1 = request.getParameter(XSLTCallingConvention.TEMPLATE_PARAMETER); String p2 = request.getParameter(XSLTCallingConvention.CLEAR_TEMPLATE_CACHE_PARAMETER); // Use the XSLT calling convention if and only if at least one of the // parameters is actually set if (! (TextUtils.isEmpty(p1) && TextUtils.isEmpty(p2))) { Log.log_3509(XSLTCallingConvention.class.getName()); return xslCC; } } // If not, see if _xins-std matches CallingConvention stdCC = getCallingConvention2("_xins-std"); if (stdCC != null && stdCC != defCC && stdCC.matchesRequest(request)) { Log.log_3509(StandardCallingConvention.class.getName()); return stdCC; } // Local variable to hold the first matching calling convention CallingConvention matching = null; // Determine which calling conventions match for (String name : _conventionNames) { Object value = getCallingConvention2(name); // if the value is null, that's maybe an initialization problem if (value == null) { value = _conventions.get(name); } // Skip all values that are not CallingConvention instances // Skip also the default and the standard calling conventions, we // already established that they cannot handle the request if (value == CREATION_FAILED || value == defCC || value == stdCC) { continue; } // Convert the value to a CallingConvention CallingConvention cc = (CallingConvention) value; // Determine whether this one can handle it if (cc.matchesRequest(request)) { // First match if (matching == null) { matching = cc; // Fail: Multiple matches } else { Log.log_3511(); String multipleMatches = "Request does not specify a calling " + "convention, it cannot be handled by the " + "default calling convention and multiple " + "calling conventions are able to handle it: \""; String message = multipleMatches + matching.getClass().getName() + "\", \"" + cc.getClass().getName() + "\"."; throw new InvalidRequestException(message); } } } // One match if (matching != null) { return matching; // Fail: No matches } else { Log.log_3510(); String noMatches = "Request does not specify a calling convention, it " + "cannot be handled by the default calling convention and it was " + "not possible to find any calling convention that can handle it."; throw new InvalidRequestException(noMatches); } } /** * Returns the set of HTTP methods supported for function invocations. This * is the union of the methods supported by the individual calling * conventions for invoking functions, so excluding the <em>OPTIONS</em> * method. The latter cannot be used for function invocations, only to * determine which HTTP methods are available. See * {@link CallingConvention#getSupportedMethods()}. * * @return * the {@link Set} of supported HTTP methods, never <code>null</code>. * * @throws IllegalStateException * if this calling convention manager is not yet bootstrapped and * initialized, see {@link #isUsable()}. */ final Set<String> getSupportedMethods() throws IllegalStateException { // Make sure this Manageable object is bootstrapped and initialized assertUsable(); HashSet<String> supportedMethods = new HashSet<String>(); for (String name : _conventionNames) { Object convention = getCallingConvention2(name); // if the value is null, that's maybe an initialization problem if (convention == null) { convention = _conventions.get(name); } // Add all methods supported by the calling convention if (convention instanceof CallingConvention) { CallingConvention cc = (CallingConvention) convention; supportedMethods.addAll(Arrays.asList(cc.getSupportedMethods())); } } return supportedMethods; } }