org.xins.server.CallingConventionManager Maven / Gradle / Ivy
/*
* $Id: CallingConventionManager.java,v 1.92 2012/03/15 21:07:39 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 CallingConvention
instances for the API.
*
* @version $Revision: 1.92 $ $Date: 2012/03/15 21:07:39 $
* @author Mees Witteman
* @author Anthony Goubard
* @author Ernst de Haan
*
* @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 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 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_CALLING_CONVENTION
});
/**
* Array of type Class
that is used when constructing a
* CallingConvention
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 null
.
*/
private static final Object CREATION_FAILED = new Object();
/**
* The API. Never null
.
*/
private final API _api;
/**
* The name of the default calling convention. There is always a default
* calling convention (at least after bootstrapping).
*
* This field is initialized during bootstrapping.
*/
private String _defaultConventionName;
/**
* The names of the possible calling conventions.
*/
private List _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 _conventions;
/**
* Creates a CallingConventionManager
for the specified API.
*
* @param api
* the API, cannot be null
.
*/
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();
_conventionNames.addAll(CONVENTIONS);
// Create a map to store the conventions in
_conventions = new HashMap(12);
}
/**
* Performs the bootstrap procedure (actual implementation).
*
* @param properties
* the bootstrap properties, not null
.
*
* @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 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 null
.
*
* @return
* the name of the default calling convention, never null
.
*
* @throws MissingRequiredPropertyException
* if a required property is not given.
*
* @throws InvalidPropertyValueException
* if the value of a certain property is invalid.
*/
private String determineDefaultConvention(Map 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
* regular and custom calling conventions.
*
* If the name does not identify a recognized calling convention, then
* null
is returned.
*
* @param properties
* the bootstrap properties, cannot be null
.
*
* @param name
* the name of the calling convention to construct, cannot be
* null
.
*
* @return
* a non-bootstrapped {@link CallingConvention} instance that matches
* the specified name, or null
if no match is found.
*/
private CallingConvention create(Map 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 regular calling
* convention is one that comes with the XINS framework.
*
* @param name
* the name of the calling convention, should not be null
* and should normally starts with an underscore character
* ('_'
).
*
* @return
* the name of the {@link CallingConvention} class that matches the
* specified calling convention name, or null
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
} else if (name.equals(JSON_CALLING_CONVENTION)) {
return "org.xins.server.JSONCallingConvention";
// Unrecognized
} else {
return null;
}
}
/**
* Constructs a new CallingConvention
instance by class name.
*
* @param name
* the name of the calling convention, cannot be null
.
*
* @param className
* the name of the class, cannot be null
.
*
* @return
* the constructed {@link CallingConvention} instance, or
* null
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 null
.
*
* @param cc
* the {@link CallingConvention} object to bootstrap, cannot be
* null
.
*
* @param properties
* the bootstrap properties, cannot be null
.
*/
private void bootstrap(String name, CallingConvention cc, Map 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 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.
*
* 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 null
.
*
* @param cc
* the {@link CallingConvention} object to initialize, cannot be
* null
.
*
* @param properties
* the initialization properties, cannot be null
.
*/
private void init(String name, CallingConvention cc, Map 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 null
.
*
* @return
* the calling convention to use, never null
.
*
* @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.
*
* The returned calling convention is bootstrapped and initialized.
*
* @param name
* the name of the calling convention to retrieve, should not be
* null
.
*
* @return
* the calling convention initialized, never null
.
*
* @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 null
if
* the calling convention is not found or not usable.
*
*
The returned calling convention is bootstrapped and initialized.
*
* @param name
* the name of the calling convention to retrieve, should not be
* null
.
*
* @return
* the calling convention, or null
.
*/
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.
*
*
The {@link CallingConvention#matchesRequest(HttpServletRequest)}
* method is used to determine which calling conventions match. Then
* the following algorithm is used to chose one:
*
*
* - if the default calling convention matches, use that;
*
- otherwise if the {@link XSLTCallingConvention} matches and at
* least one of the parameters specific for the this calling
* convention is set, then use it;
*
- otherwise if the {@link StandardCallingConvention} matches, use
* that;
*
- otherwise if there is exactly one other calling convention that
* matches, use that one;
*
- if none of the calling conventions match, throw an
* {@link InvalidRequestException}, indicating that no match could
* be found;
*
- if multiple calling conventions match, throw an
* {@link InvalidRequestException}, indicating that several matches
* were found;
*
*
* @param request
* the incoming request, cannot be null
.
*
* @return
* the calling convention to use, never null
.
*
* @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 OPTIONS
* 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 null
.
*
* @throws IllegalStateException
* if this calling convention manager is not yet bootstrapped and
* initialized, see {@link #isUsable()}.
*/
final Set getSupportedMethods() throws IllegalStateException {
// Make sure this Manageable object is bootstrapped and initialized
assertUsable();
HashSet supportedMethods = new HashSet();
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;
}
}