0001: /*
0002: * $Id: Engine.java,v 1.112 2007/09/24 12:46:37 agoubard Exp $
0003: *
0004: * Copyright 2003-2007 Orange Nederland Breedband B.V.
0005: * See the COPYRIGHT file for redistribution and use restrictions.
0006: */
0007: package org.xins.server;
0008:
0009: import java.io.File;
0010: import java.io.FileNotFoundException;
0011: import java.io.IOException;
0012: import java.io.InputStream;
0013: import java.io.Writer;
0014: import java.net.MalformedURLException;
0015: import java.net.URL;
0016: import java.util.Arrays;
0017: import java.util.Iterator;
0018: import java.util.Map;
0019: import java.util.Set;
0020:
0021: import javax.servlet.ServletConfig;
0022: import javax.servlet.ServletContext;
0023: import javax.servlet.ServletException;
0024: import javax.servlet.http.HttpServletRequest;
0025: import javax.servlet.http.HttpServletResponse;
0026:
0027: import org.apache.log4j.NDC;
0028:
0029: import org.apache.oro.text.regex.MalformedPatternException;
0030: import org.apache.oro.text.regex.Pattern;
0031: import org.apache.oro.text.regex.Perl5Compiler;
0032: import org.apache.oro.text.regex.Perl5Matcher;
0033:
0034: import org.json.JSONArray;
0035: import org.json.JSONException;
0036: import org.json.JSONObject;
0037: import org.xins.common.FormattedParameters;
0038:
0039: import org.xins.common.MandatoryArgumentChecker;
0040: import org.xins.common.Utils;
0041: import org.xins.common.collections.InvalidPropertyValueException;
0042: import org.xins.common.collections.MissingRequiredPropertyException;
0043: import org.xins.common.collections.PropertyReader;
0044: import org.xins.common.io.IOReader;
0045: import org.xins.common.manageable.InitializationException;
0046: import org.xins.common.spec.APISpec;
0047: import org.xins.common.spec.EntityNotFoundException;
0048: import org.xins.common.spec.InvalidSpecificationException;
0049: import org.xins.common.text.DateConverter;
0050: import org.xins.common.text.TextUtils;
0051:
0052: import org.xins.logdoc.ExceptionUtils;
0053: import org.xins.logdoc.LogCentral;
0054:
0055: /**
0056: * XINS server engine. The engine is a delegate of the {@link APIServlet} that
0057: * is responsible for initialization and request handling.
0058: *
0059: * @version $Revision: 1.112 $ $Date: 2007/09/24 12:46:37 $
0060: * @author <a href="mailto:ernst@ernstdehaan.com">Ernst de Haan</a>
0061: * @author <a href="mailto:anthony.goubard@japplis.com">Anthony Goubard</a>
0062: * @author <a href="mailto:mees.witteman@orange-ftgroup.com">Mees Witteman</a>
0063: */
0064: final class Engine {
0065:
0066: /**
0067: * Class used to convert dates to String.
0068: */
0069: private static final DateConverter DATE_CONVERTER = new DateConverter(
0070: true);
0071:
0072: /**
0073: * Perl 5 pattern compiler.
0074: */
0075: private static final Perl5Compiler PATTERN_COMPILER = new Perl5Compiler();
0076:
0077: /**
0078: * Property used to start JMX.
0079: */
0080: private static final String JMX_PROPERTY = "org.xins.server.jmx";
0081:
0082: /**
0083: * The state machine for this engine. Never <code>null</code>.
0084: */
0085: private final EngineStateMachine _stateMachine = new EngineStateMachine();
0086:
0087: /**
0088: * The starter of this engine. Never <code>null</code>.
0089: */
0090: private final EngineStarter _starter;
0091:
0092: /**
0093: * The stored servlet configuration object. Never <code>null</code>.
0094: */
0095: private final ServletConfig _servletConfig;
0096:
0097: /**
0098: * The API that this engine forwards requests to. Never <code>null</code>.
0099: */
0100: private final API _api;
0101:
0102: /**
0103: * Diagnostic context ID generator. Never <code>null</code>.
0104: */
0105: private ContextIDGenerator _contextIDGenerator;
0106:
0107: /**
0108: * The name of the API. Never <code>null</code>.
0109: */
0110: private String _apiName;
0111:
0112: /**
0113: * The manager for the runtime configuration file. Never <code>null</code>.
0114: */
0115: private final ConfigManager _configManager;
0116:
0117: /**
0118: * The manager for the calling conventions. This field can be and initially
0119: * is <code>null</code>. This field is initialized by
0120: * {@link #bootstrapAPI()}.
0121: */
0122: private CallingConventionManager _conventionManager;
0123:
0124: /**
0125: * Pattern which incoming diagnostic context identifiers must match. Can be
0126: * <code>null</code> in case no pattern has been specified. Initially this
0127: * field is indeed <code>null</code>.
0128: */
0129: private Pattern _contextIDPattern;
0130:
0131: /**
0132: * The SMD (Simple Method Description) of this API. This value is <code>null</code>
0133: * until the meta function <i>_SMD</i> is called.
0134: */
0135: private String _smd;
0136:
0137: /**
0138: * Constructs a new <code>Engine</code> object.
0139: *
0140: * @param config
0141: * the {@link ServletConfig} object which contains build-time properties
0142: * for this servlet, cannot be <code>null</code>.
0143: *
0144: * @throws IllegalArgumentException
0145: * if <code>config == null</code>.
0146: *
0147: * @throws ServletException
0148: * if the engine could not be constructed.
0149: */
0150: Engine(ServletConfig config) throws IllegalArgumentException,
0151: ServletException {
0152:
0153: // Check preconditions
0154: MandatoryArgumentChecker.check("config", config);
0155:
0156: // Construct the EngineStarter
0157: _starter = new EngineStarter(config);
0158:
0159: // Construct a configuration manager and store the servlet configuration
0160: _configManager = new ConfigManager(this , config);
0161: _servletConfig = config;
0162:
0163: // Proceed to first actual stage
0164: _stateMachine.setState(EngineState.BOOTSTRAPPING_FRAMEWORK);
0165:
0166: // Read configuration details
0167: _configManager.determineConfigFile();
0168: _configManager.readRuntimeProperties();
0169: if (!_configManager.propertiesRead()) {
0170: _stateMachine
0171: .setState(EngineState.FRAMEWORK_BOOTSTRAP_FAILED);
0172: throw new ServletException();
0173: }
0174:
0175: // Log boot messages
0176: _starter.logBootMessages();
0177:
0178: // Construct and bootstrap the API
0179: _stateMachine.setState(EngineState.CONSTRUCTING_API);
0180: try {
0181: _api = _starter.constructAPI();
0182: } catch (ServletException se) {
0183: _stateMachine.setState(EngineState.API_CONSTRUCTION_FAILED);
0184: throw se;
0185: }
0186: boolean bootstrapped = bootstrapAPI();
0187: if (!bootstrapped) {
0188: throw new ServletException();
0189: }
0190:
0191: // Done bootstrapping the framework
0192: Log.log_3225(Library.getVersion());
0193:
0194: // Initialize the configuration manager
0195: _configManager.init();
0196:
0197: // Check post-conditions
0198: if (_api == null) {
0199: throw Utils.logProgrammingError("_api == null");
0200: } else if (_apiName == null) {
0201: throw Utils.logProgrammingError("_apiName == null");
0202: }
0203: }
0204:
0205: /**
0206: * Bootstraps the API. The following steps will be performed:
0207: *
0208: * <ul>
0209: * <li>determine the API name;
0210: * <li>load the Logdoc, if available;
0211: * <li>bootstrap the API;
0212: * <li>construct and bootstrap the calling conventions;
0213: * <li>link the engine to the API;
0214: * <li>construct and bootstrap a context ID generator;
0215: * <li>perform JMX initialization.
0216: * </ul>
0217: *
0218: * @return
0219: * <code>true</code> if the bootstrapping of the API succeeded,
0220: * <code>false</code> if it failed.
0221: */
0222: private boolean bootstrapAPI() {
0223:
0224: // Proceed to next stage
0225: _stateMachine.setState(EngineState.BOOTSTRAPPING_API);
0226:
0227: // Make the API have a link to this Engine
0228: _api.setEngine(this );
0229:
0230: PropertyReader bootProps;
0231: try {
0232:
0233: // Determine the name of the API
0234: _apiName = _starter.determineAPIName();
0235:
0236: // Load the Logdoc if available
0237: _starter.loadLogdoc();
0238:
0239: // Actually bootstrap the API
0240: bootProps = _starter.bootstrap(_api);
0241:
0242: // Handle any failures
0243: } catch (ServletException se) {
0244: _stateMachine.setState(EngineState.API_BOOTSTRAP_FAILED);
0245: return false;
0246: }
0247:
0248: // Create the calling convention manager
0249: _conventionManager = new CallingConventionManager(_api);
0250:
0251: // Bootstrap the calling convention manager
0252: try {
0253: _conventionManager.bootstrap(bootProps);
0254:
0255: // Missing required property
0256: } catch (MissingRequiredPropertyException exception) {
0257: Log.log_3209(exception.getPropertyName(), exception
0258: .getDetail());
0259: return false;
0260:
0261: // Invalid property value
0262: } catch (InvalidPropertyValueException exception) {
0263: Log.log_3210(exception.getPropertyName(), exception
0264: .getPropertyValue(), exception.getReason());
0265: return false;
0266:
0267: // Other bootstrap error
0268: } catch (Throwable exception) {
0269: Log.log_3211(exception);
0270: return false;
0271: }
0272:
0273: // Construct a generator for diagnostic context IDs
0274: _contextIDGenerator = new ContextIDGenerator(_api.getName());
0275: try {
0276: _contextIDGenerator.bootstrap(bootProps);
0277: } catch (Exception exception) {
0278: return false;
0279: }
0280:
0281: // Perform JMX initialization if asked
0282: String enableJmx = _configManager.getRuntimeProperties().get(
0283: JMX_PROPERTY);
0284: if ("true".equals(enableJmx)) {
0285: _starter.registerMBean(_api);
0286: } else if (enableJmx != null && !enableJmx.equals("false")) {
0287: Log.log_3251(enableJmx);
0288: return false;
0289: }
0290:
0291: // Succeeded
0292: return true;
0293: }
0294:
0295: /**
0296: * Initializes the API using the current runtime settings. This method
0297: * should be called whenever the runtime properties changed.
0298: *
0299: * @return
0300: * <code>true</code> if the initialization succeeded, otherwise
0301: * <code>false</code>.
0302: */
0303: boolean initAPI() {
0304:
0305: _stateMachine.setState(EngineState.INITIALIZING_API);
0306:
0307: // Determine the locale for logging
0308: boolean localeInitialized = _configManager.determineLogLocale();
0309: if (!localeInitialized) {
0310: _stateMachine
0311: .setState(EngineState.API_INITIALIZATION_FAILED);
0312: return false;
0313: }
0314:
0315: // Check that the runtime properties were correct
0316: if (!_configManager.propertiesRead()) {
0317: _stateMachine
0318: .setState(EngineState.API_INITIALIZATION_FAILED);
0319: return false;
0320: }
0321:
0322: // Determine the current runtime properties
0323: PropertyReader properties = _configManager
0324: .getRuntimeProperties();
0325:
0326: // Determine at what level should the stack traces be displayed
0327: String stackTraceAtMessageLevel = properties
0328: .get(LogCentral.LOG_STACK_TRACE_AT_MESSAGE_LEVEL);
0329: if ("true".equals(stackTraceAtMessageLevel)) {
0330: LogCentral.setStackTraceAtMessageLevel(true);
0331: } else if ("false".equals(stackTraceAtMessageLevel)) {
0332: LogCentral.setStackTraceAtMessageLevel(false);
0333: } else if (stackTraceAtMessageLevel != null) {
0334: // XXX: Report this error in some way
0335: _stateMachine
0336: .setState(EngineState.API_INITIALIZATION_FAILED);
0337: return false;
0338: }
0339:
0340: boolean succeeded = false;
0341:
0342: try {
0343:
0344: // Determine filter for incoming diagnostic context IDs
0345: _contextIDPattern = determineContextIDPattern(properties);
0346:
0347: // Initialize the diagnostic context ID generator
0348: _contextIDGenerator.init(properties);
0349:
0350: // Initialize the API
0351: _api.init(properties);
0352:
0353: // Initialize the default calling convention for this API
0354: _conventionManager.init(properties);
0355:
0356: succeeded = true;
0357:
0358: // Missing required property
0359: } catch (MissingRequiredPropertyException exception) {
0360: Log.log_3411(exception.getPropertyName(), exception
0361: .getDetail());
0362:
0363: // Invalid property value
0364: } catch (InvalidPropertyValueException exception) {
0365: Log.log_3412(exception.getPropertyName(), exception
0366: .getPropertyValue(), exception.getReason());
0367:
0368: // Initialization of API failed for some other reason
0369: } catch (InitializationException exception) {
0370: Log.log_3413(exception);
0371:
0372: // Other error
0373: } catch (Throwable exception) {
0374: Log.log_3414(exception);
0375:
0376: // Always leave the object in a well-known state
0377: } finally {
0378: if (succeeded) {
0379: _stateMachine.setState(EngineState.READY);
0380: } else {
0381: _stateMachine
0382: .setState(EngineState.API_INITIALIZATION_FAILED);
0383: }
0384: }
0385:
0386: return succeeded;
0387: }
0388:
0389: /**
0390: * Determines the filter for diagnostic context identifiers.
0391: *
0392: * @param properties
0393: * the runtime properties to retrieve information from, cannot be
0394: * <code>null</code>.
0395: *
0396: * @return
0397: * the filter as a {@link Pattern} object, or <code>null</code> if no
0398: * filter is specified.
0399: *
0400: * @throws IllegalArgumentException
0401: * if <code>properties == null</code>.
0402: *
0403: * @throws InvalidPropertyValueException
0404: * if the value for the filter property is considered invalid.
0405: */
0406: private Pattern determineContextIDPattern(PropertyReader properties)
0407: throws IllegalArgumentException,
0408: InvalidPropertyValueException {
0409:
0410: // Check preconditions
0411: MandatoryArgumentChecker.check("properties", properties);
0412:
0413: // Determine pattern string
0414: // XXX: Store "org.xins.server.contextID.filter" in a constant
0415: String propName = "org.xins.server.contextID.filter";
0416: String propValue = properties.get(propName);
0417:
0418: // If the property value is empty, then there is no pattern
0419: Pattern pattern;
0420: if (TextUtils.isEmpty(propValue)) {
0421: pattern = null;
0422: Log.log_3431();
0423:
0424: // Otherwise we must provide a Pattern instance
0425: } else {
0426:
0427: // Convert the string to a Pattern
0428: try {
0429: // XXX: Why is the pattern made case-insensitive?
0430: int mask = Perl5Compiler.READ_ONLY_MASK
0431: | Perl5Compiler.CASE_INSENSITIVE_MASK;
0432: pattern = PATTERN_COMPILER.compile(propValue, mask);
0433: Log.log_3432(propValue);
0434:
0435: // Malformed pattern indicates an invalid value
0436: } catch (MalformedPatternException exception) {
0437: Log.log_3433(propValue);
0438: InvalidPropertyValueException ipve;
0439: ipve = new InvalidPropertyValueException(propName,
0440: propValue);
0441: ExceptionUtils.setCause(ipve, exception);
0442: throw ipve;
0443: }
0444: }
0445:
0446: return pattern;
0447: }
0448:
0449: /**
0450: * Handles a request to this servlet (wrapper method). If any of the
0451: * arguments is <code>null</code>, then the behaviour of this method is
0452: * undefined.
0453: *
0454: * @param request
0455: * the servlet request, should not be <code>null</code>.
0456: *
0457: * @param response
0458: * the servlet response, should not be <code>null</code>.
0459: *
0460: * @throws IOException
0461: * in case of an I/O error.
0462: */
0463: void service(HttpServletRequest request,
0464: HttpServletResponse response) throws IOException {
0465:
0466: // Set the correct character encoding for the request
0467: if (request.getCharacterEncoding() == null) {
0468: request.setCharacterEncoding("UTF-8");
0469: }
0470:
0471: // Associate the current diagnostic context identifier with this thread
0472: String contextID = determineContextID(request);
0473: NDC.push(contextID);
0474:
0475: // Handle the request
0476: try {
0477: doService(request, response);
0478:
0479: // Catch and log all exceptions
0480: } catch (Throwable exception) {
0481: Log.log_3003(exception);
0482:
0483: // Finally always disassociate the diagnostic context identifier from
0484: // this thread
0485: } finally {
0486: NDC.pop();
0487: NDC.remove();
0488: }
0489: }
0490:
0491: /**
0492: * Returns an applicable diagnostic context identifier. If the request
0493: * already specifies a diagnostic context identifier, then that will be
0494: * used. Otherwise a new one will be generated.
0495: *
0496: * @param request
0497: * the HTTP servlet request, should not be <code>null</code>.
0498: *
0499: * @return
0500: * the diagnostic context identifier, never <code>null</code> and never
0501: * an empty string.
0502: */
0503: private String determineContextID(HttpServletRequest request) {
0504:
0505: // See if the request already specifies a diagnostic context identifier
0506: // XXX: Store "_context" in a constant
0507: String contextID = request.getParameter("_context");
0508: if (TextUtils.isEmpty(contextID)) {
0509: contextID = _contextIDGenerator.generate();
0510: Log.log_3583(contextID);
0511:
0512: // Indeed there is a context ID in the request, make sure it's valid
0513: } else {
0514:
0515: // Valid context ID
0516: if (isValidContextID(contextID)) {
0517: Log.log_3581(contextID);
0518:
0519: // Invalid context ID
0520: } else {
0521: Log.log_3582(contextID);
0522: contextID = null;
0523: }
0524: }
0525:
0526: return contextID;
0527: }
0528:
0529: /**
0530: * Determines if the specified incoming context identifier is considered
0531: * valid.
0532: *
0533: * @param contextID
0534: * the incoming diagnostic context identifier, should not be
0535: * <code>null</code>.
0536: *
0537: * @return
0538: * <code>true</code> if <code>contextID</code> is considered acceptable,
0539: * <code>false</code> if it is considered unacceptable.
0540: */
0541: private boolean isValidContextID(String contextID) {
0542:
0543: // If a filter is specified, validate that the ID matches it
0544: if (_contextIDPattern != null) {
0545: Perl5Matcher matcher = new Perl5Matcher();
0546: return matcher.matches(contextID, _contextIDPattern);
0547:
0548: // No filter is specified, everything is allowed
0549: } else {
0550: return true;
0551: }
0552: }
0553:
0554: /**
0555: * Handles a request to this servlet (implementation method). If any of the
0556: * arguments is <code>null</code>, then the behaviour of this method is
0557: * undefined.
0558: *
0559: * <p>This method is called from the corresponding wrapper method,
0560: * {@link #service(HttpServletRequest,HttpServletResponse)}.
0561: *
0562: * @param request
0563: * the servlet request, should not be <code>null</code>.
0564: *
0565: * @param response
0566: * the servlet response, should not be <code>null</code>.
0567: *
0568: * @throws IOException
0569: * in case of an I/O error.
0570: */
0571: private void doService(HttpServletRequest request,
0572: HttpServletResponse response) throws IOException {
0573:
0574: // Determine current time
0575: long start = System.currentTimeMillis();
0576:
0577: // Log that we have received an HTTP request
0578: String remoteIP = request.getRemoteAddr();
0579: String method = request.getMethod();
0580: String path = request.getRequestURI();
0581: String queryString = request.getQueryString();
0582: Log.log_3521(remoteIP, method, path, queryString);
0583:
0584: // If the current state is not usable, then return an error immediately
0585: EngineState state = _stateMachine.getState();
0586: if (!state.allowsInvocations()) {
0587: handleUnusableState(state, request, response);
0588:
0589: // Support the HTTP method "OPTIONS"
0590: } else if ("OPTIONS".equals(method)) {
0591: if ("*".equals(path)) {
0592: handleOptions(null, request, response);
0593: } else {
0594: delegateToCC(start, request, response);
0595: }
0596:
0597: // The request should be handled by a calling convention
0598: } else {
0599: delegateToCC(start, request, response);
0600: }
0601: }
0602:
0603: /**
0604: * Handles an unprocessable request (low-level function). The response is
0605: * filled for the request.
0606: *
0607: * @param request
0608: * the HTTP request, cannot be <code>null</code>.
0609: *
0610: * @param response
0611: * the HTTP request, cannot be <code>null</code>.
0612: *
0613: * @param statusCode
0614: * the HTTP status code to return.
0615: *
0616: * @param reason
0617: * explanation, can be <code>null</code>.
0618: *
0619: * @param exception
0620: * the exception thrown, can be <code>null</code>.
0621: *
0622: * @throws IOException
0623: * in case of an I/O error.
0624: */
0625: private void handleUnprocessableRequest(HttpServletRequest request,
0626: HttpServletResponse response, int statusCode,
0627: String reason, Throwable exception) throws IOException {
0628:
0629: // Log
0630: Log.log_3523(exception, request.getRemoteAddr(), request
0631: .getMethod(), request.getRequestURI(), request
0632: .getQueryString(), statusCode, reason);
0633:
0634: // Send the HTTP status code to the client
0635: response.sendError(statusCode);
0636: }
0637:
0638: /**
0639: * Handles a request that comes in while function invocations are currently
0640: * not allowed.
0641: *
0642: * @param state
0643: * the current state, cannot be <code>null</code>.
0644: *
0645: * @param request
0646: * the HTTP request, cannot be <code>null</code>.
0647: *
0648: * @param response
0649: * the HTTP response to fill, cannot be <code>null</code>.
0650: *
0651: * @throws IOException
0652: * in case of an I/O error.
0653: */
0654: private void handleUnusableState(EngineState state,
0655: HttpServletRequest request, HttpServletResponse response)
0656: throws IOException {
0657:
0658: // Log and respond
0659: int statusCode = state.isError() ? HttpServletResponse.SC_INTERNAL_SERVER_ERROR
0660: : HttpServletResponse.SC_SERVICE_UNAVAILABLE;
0661: String reason = "XINS/Java Server Framework engine state \""
0662: + state + "\" does not allow incoming requests.";
0663: handleUnprocessableRequest(request, response, statusCode,
0664: reason, null);
0665: }
0666:
0667: /**
0668: * Delegates the specified incoming request to the appropriate
0669: * <code>CallingConvention</code>. The request may either be a function
0670: * invocation or an <em>OPTIONS</em> request.
0671: *
0672: * @param start
0673: * timestamp indicating when the call was received by the framework, in
0674: * milliseconds since the
0675: * <a href="http://en.wikipedia.org/wiki/Unix_Epoch">UNIX Epoch</a>.
0676: *
0677: * @param request
0678: * the servlet request, should not be <code>null</code>.
0679: *
0680: * @param response
0681: * the servlet response, should not be <code>null</code>.
0682: *
0683: * @throws IOException
0684: * in case of an I/O error.
0685: */
0686: private void delegateToCC(long start, HttpServletRequest request,
0687: HttpServletResponse response) throws IOException {
0688:
0689: // Determine the calling convention to use
0690: CallingConvention cc = determineCC(request, response);
0691:
0692: // If it is null, then there was an error. This error will have been
0693: // handled completely, including logging and response output.
0694: if (cc != null) {
0695:
0696: // Handle OPTIONS calls separately
0697: String method = request.getMethod();
0698: if ("OPTIONS".equals(method)) {
0699: handleOptions(cc, request, response);
0700:
0701: // Non-OPTIONS requests are function invocations
0702: } else {
0703: invokeFunction(start, cc, request, response);
0704: }
0705: }
0706: }
0707:
0708: /**
0709: * Determines which calling convention should be used for the specified
0710: * request. In case of an error, an error response will be produced and
0711: * sent to the client.
0712: *
0713: * @param request
0714: * the HTTP request for which to determine the calling convention to use
0715: * cannot be <code>null</code>.
0716: *
0717: * @param response
0718: * the HTTP response, cannot be <code>null</code>.
0719: *
0720: * @return
0721: * the {@link CallingConvention} to use, or <code>null</code> if the
0722: * calling convention to use could not be determined.
0723: *
0724: * @throws IOException
0725: * in case of an I/O error.
0726: */
0727: private final CallingConvention determineCC(
0728: HttpServletRequest request, HttpServletResponse response)
0729: throws IOException {
0730:
0731: // Determine the calling convention; if an existing calling convention
0732: // is specified in the request, then use that, otherwise use the default
0733: // calling convention for this engine
0734: CallingConvention cc = null;
0735: try {
0736: cc = _conventionManager.getCallingConvention(request);
0737:
0738: // Only an InvalidRequestException is expected. If a different kind of
0739: // exception is received, then that is considered a programming error.
0740: } catch (Throwable exception) {
0741: int statusCode;
0742: String reason;
0743: if (exception instanceof InvalidRequestException) {
0744:
0745: String method = request.getMethod();
0746: String ccName = request
0747: .getParameter(CallingConventionManager.CALLING_CONVENTION_PARAMETER);
0748: // Check if the method is known by at least one CC (otherwise 501)
0749: if (!_conventionManager.getSupportedMethods().contains(
0750: method)) {
0751: statusCode = HttpServletResponse.SC_NOT_IMPLEMENTED;
0752: reason = "The HTTP method \""
0753: + method
0754: + "\" is not known by any of the usable calling conventions.";
0755:
0756: // Check if the method is known for the specified CC (otherwise 405)
0757: } else if (ccName != null
0758: && _conventionManager
0759: .getCallingConvention2(ccName) != null
0760: && !Arrays.asList(
0761: _conventionManager
0762: .getCallingConvention2(ccName)
0763: .getSupportedMethods(request))
0764: .contains(method)) {
0765: statusCode = HttpServletResponse.SC_METHOD_NOT_ALLOWED;
0766: reason = "The HTTP method \""
0767: + method
0768: + "\" is not allowed for the calling convention \""
0769: + ccName + "\".";
0770: } else {
0771: statusCode = HttpServletResponse.SC_BAD_REQUEST;
0772: reason = "Unable to activate appropriate calling convention";
0773: String exceptionMessage = exception.getMessage();
0774: if (TextUtils.isEmpty(exceptionMessage)) {
0775: reason += '.';
0776: } else {
0777: reason += ": " + exceptionMessage;
0778: }
0779: }
0780: } else {
0781: statusCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
0782: reason = "Internal error while trying to determine "
0783: + "appropriate calling convention.";
0784: }
0785:
0786: // Log and respond
0787: handleUnprocessableRequest(request, response, statusCode,
0788: reason, exception);
0789: }
0790:
0791: return cc;
0792: }
0793:
0794: /**
0795: * Invokes a function, using the specified calling convention to from an
0796: * HTTP request and to an HTTP response.
0797: *
0798: * @param start
0799: * timestamp indicating when the call was received by the framework, in
0800: * milliseconds since the
0801: * <a href="http://en.wikipedia.org/wiki/Unix_Epoch">UNIX Epoch</a>.
0802: *
0803: * @param cc
0804: * the calling convention to use, cannot be <code>null</code>.
0805: *
0806: * @param request
0807: * the HTTP request, cannot be <code>null</code>.
0808: *
0809: * @param response
0810: * the HTTP response, cannot be <code>null</code>.
0811: *
0812: * @throws IOException
0813: * in case of an I/O error.
0814: */
0815: private void invokeFunction(long start, CallingConvention cc,
0816: HttpServletRequest request, HttpServletResponse response)
0817: throws IOException {
0818:
0819: // Convert the HTTP request to a XINS request
0820: FunctionRequest xinsRequest;
0821: try {
0822: xinsRequest = cc.convertRequest(request);
0823:
0824: // Only an InvalidRequestException or a FunctionNotSpecifiedException is
0825: // expected. If a different kind of exception is received, then that is
0826: // considered a programming error.
0827: } catch (Throwable exception) {
0828:
0829: int statusCode;
0830: String reason;
0831:
0832: if (exception instanceof InvalidRequestException) {
0833: statusCode = HttpServletResponse.SC_BAD_REQUEST;
0834: reason = "Calling convention \""
0835: + cc.getClass().getName()
0836: + "\" cannot process the request";
0837: String exceptionMessage = exception.getMessage();
0838: if (!TextUtils.isEmpty(exceptionMessage)) {
0839: reason += ": " + exceptionMessage;
0840: } else {
0841: reason += '.';
0842: }
0843: } else if (exception instanceof FunctionNotSpecifiedException) {
0844: statusCode = HttpServletResponse.SC_NOT_FOUND;
0845: reason = "Cannot determine which function to invoke.";
0846: } else {
0847: statusCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
0848: reason = "Internal error.";
0849: }
0850:
0851: // Log and respond
0852: handleUnprocessableRequest(request, response, statusCode,
0853: reason, exception);
0854:
0855: return;
0856: }
0857:
0858: // Do not handle the call if the API is disabled
0859: if (_api.isDisabled()
0860: && !"_EnableAPI".equals(xinsRequest.getFunctionName())) {
0861: response
0862: .sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
0863: return;
0864: }
0865:
0866: // Call the function
0867: FunctionResult result;
0868: try {
0869: result = _api.handleCall(start, xinsRequest, request
0870: .getRemoteAddr(), cc);
0871:
0872: // The only expected exceptions are NoSuchFunctionException and
0873: // AccessDeniedException. Other exceptions are considered to indicate
0874: // a programming error.
0875: } catch (Throwable exception) {
0876:
0877: int statusCode;
0878: String reason;
0879:
0880: // Access denied
0881: if (exception instanceof AccessDeniedException) {
0882: statusCode = HttpServletResponse.SC_FORBIDDEN;
0883: reason = "Access is denied.";
0884:
0885: // No such function
0886: } else if (exception instanceof NoSuchFunctionException) {
0887: statusCode = HttpServletResponse.SC_NOT_FOUND;
0888: reason = "The specified function \""
0889: + xinsRequest.getFunctionName()
0890: + "\" is unknown.";
0891:
0892: // Internal error
0893: } else {
0894: statusCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
0895: reason = "Internal error while processing function call.";
0896: }
0897:
0898: // Log and respond
0899: handleUnprocessableRequest(request, response, statusCode,
0900: reason, exception);
0901:
0902: return;
0903: }
0904:
0905: // Shortcut for the _WSDL meta function
0906: if (xinsRequest.getFunctionName().equals("_WSDL")) {
0907: handleWsdlRequest(response);
0908: return;
0909: }
0910:
0911: // Shortcut for the _SMD meta function
0912: if (xinsRequest.getFunctionName().equals("_SMD")) {
0913: handleSmdRequest(request, response);
0914: return;
0915: }
0916:
0917: // Convert the XINS result to an HTTP response
0918: try {
0919: cc.convertResult(result, response, request);
0920:
0921: // NOTE: If the convertResult method throws an exception, then it
0922: // will have been logged within the CallingConvention class
0923: // already.
0924: } catch (Throwable exception) {
0925: response
0926: .sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
0927: return;
0928: }
0929: }
0930:
0931: /**
0932: * Handles an <em>OPTIONS</em> request for a specific calling convention
0933: * or for the resource <code>*</code> if no calling convention is given.
0934: *
0935: * @param cc
0936: * the calling convention, can be <code>null</code>. if no calling
0937: * convention is specified all possible method names are returned.
0938: *
0939: * @param request
0940: * the request, never <code>null</code>.
0941: *
0942: * @param response
0943: * the response to fill, never <code>null</code>.
0944: *
0945: * @throws IOException
0946: * in case of an I/O error.
0947: */
0948: private void handleOptions(CallingConvention cc,
0949: HttpServletRequest request, HttpServletResponse response)
0950: throws IOException {
0951:
0952: // Create the string with the supported HTTP methods
0953: String[] methods;
0954: if (cc != null) {
0955: methods = cc.getSupportedMethods(request);
0956: } else {
0957: Set supportedMethods = _conventionManager
0958: .getSupportedMethods();
0959: methods = (String[]) supportedMethods
0960: .toArray(new String[supportedMethods.size()]);
0961: }
0962: String methodsList = "OPTIONS";
0963: for (int i = 0; i < methods.length; i++) {
0964:
0965: methodsList += ", " + methods[i];
0966: }
0967:
0968: // Return the full response
0969: response.setStatus(HttpServletResponse.SC_OK);
0970: response.setHeader("Accept", methodsList);
0971: response.setContentLength(0);
0972: }
0973:
0974: /**
0975: * Destroys this servlet. A best attempt will be made to release all
0976: * resources.
0977: *
0978: * <p>After this method has finished, it will set the state to
0979: * <em>disposed</em>. In that state no more requests will be handled.
0980: */
0981: void destroy() {
0982:
0983: // Log: Shutting down XINS/Java Server Framework
0984: Log.log_3600();
0985:
0986: // Set the state temporarily to DISPOSING
0987: _stateMachine.setState(EngineState.DISPOSING);
0988:
0989: // Destroy the configuration manager
0990: if (_configManager != null) {
0991: try {
0992: _configManager.destroy();
0993: } catch (Throwable exception) {
0994: Utils.logIgnoredException(exception);
0995: }
0996: }
0997:
0998: // Destroy the API
0999: if (_api != null) {
1000: try {
1001: _api.deinit();
1002: } catch (Throwable exception) {
1003: Utils.logIgnoredException(exception);
1004: }
1005: }
1006:
1007: // Set the state to DISPOSED
1008: _stateMachine.setState(EngineState.DISPOSED);
1009:
1010: // Log: Shutdown completed
1011: Log.log_3602();
1012: }
1013:
1014: /**
1015: * Re-initializes the configuration file listener if there is no file
1016: * watcher; otherwise interrupts the file watcher.
1017: */
1018: void reloadPropertiesIfChanged() {
1019: _configManager.reloadPropertiesIfChanged();
1020: }
1021:
1022: /**
1023: * Returns the <code>ServletConfig</code> object which contains the
1024: * build-time properties for this servlet. The returned
1025: * {@link ServletConfig} object is the one that was passed to the
1026: * constructor.
1027: *
1028: * @return
1029: * the {@link ServletConfig} object that was used to initialize this
1030: * servlet, never <code>null</code>.
1031: */
1032: ServletConfig getServletConfig() {
1033: return _servletConfig;
1034: }
1035:
1036: /**
1037: * Gets the location of a file or a directory included in the WAR file.
1038: *
1039: * @param path
1040: * the relative path in the WAR to locate the file or the directory.
1041: *
1042: * @return
1043: * the String representation of the URL of the given path or <code>null</code>
1044: * if the path cannot be found.
1045: */
1046: String getFileLocation(String path) {
1047: String baseURL = null;
1048: ServletConfig config = getServletConfig();
1049: ServletContext context = config.getServletContext();
1050: try {
1051: String realPath = context.getRealPath(path);
1052: if (realPath != null) {
1053: baseURL = new File(realPath).toURL().toExternalForm();
1054: } else {
1055: URL pathURL = context.getResource(path);
1056: if (pathURL == null) {
1057: pathURL = getClass().getResource(path);
1058: }
1059: if (pathURL != null) {
1060: baseURL = pathURL.toExternalForm();
1061: } else {
1062: Log.log_3517(path, null);
1063: }
1064: }
1065: } catch (MalformedURLException muex) {
1066: // Let the base URL be null
1067: Log.log_3517(path, muex.getMessage());
1068: }
1069: return baseURL;
1070: }
1071:
1072: /**
1073: * Gets the resource in the WAR file.
1074: *
1075: * @param path
1076: * the path for the resource, cannot be <code>null</code> and should start with /.
1077: *
1078: * @return
1079: * the InputStream to use to read this resource or <code>null</code> if
1080: * the resource cannot be found.
1081: *
1082: * @throws IllegalArgumentException
1083: * if <code>path == null</code> or if the path doesn't start with /.
1084: *
1085: * @since XINS 2.1.
1086: */
1087: InputStream getResourceAsStream(String path)
1088: throws IllegalArgumentException {
1089: MandatoryArgumentChecker.check("path", path);
1090: if (!path.startsWith("/")) {
1091: throw new IllegalArgumentException("The path '" + path
1092: + "' should start with /.");
1093: }
1094: String resource = getFileLocation(path);
1095: if (resource != null) {
1096: try {
1097: return new URL(resource).openStream();
1098: } catch (IOException ioe) {
1099: // Fall through and return null
1100: }
1101: }
1102: return null;
1103: }
1104:
1105: /**
1106: * Logs the specified transaction.
1107: *
1108: * @param request
1109: * the {@link FunctionRequest}, should not be <code>null</code>.
1110: *
1111: * @param result
1112: * the {@link FunctionResult}, should not be <code>null</code>.
1113: *
1114: * @param ip
1115: * the client IP address, should not be <code>null</code>.
1116: *
1117: * @param start
1118: * the start of the call, a timestamp as milliseconds since the Epoch.
1119: *
1120: * @param duration
1121: * the duration of the call, in milliseconds.
1122: *
1123: * @throws NullPointerException
1124: * if <code>request == null || result == null</code>.
1125: */
1126: static void logTransaction(FunctionRequest request,
1127: FunctionResult result, String ip, long start, long duration)
1128: throws NullPointerException {
1129:
1130: // Determine the name of the function that was requested to be invoked
1131: String functionName = request.getFunctionName();
1132:
1133: // Determine error code, fallback is a zero character
1134: String code = result.getErrorCode();
1135: if (code == null || code.length() < 1) {
1136: code = "0";
1137: }
1138:
1139: // Prepare for transaction logging
1140: String serStart = DATE_CONVERTER.format(start);
1141: Object inParams = new FormattedParameters(request
1142: .getParameters(), request.getDataElement());
1143: Object outParams = new FormattedParameters(result
1144: .getParameters(), result.getDataElement());
1145:
1146: // Log transaction before returning the result
1147: Log.log_3540(serStart, ip, functionName, duration, code,
1148: inParams, outParams);
1149: Log.log_3541(serStart, ip, functionName, duration, code);
1150: }
1151:
1152: /**
1153: * Handles the request for the _WSDL meta function.
1154: *
1155: * @param response
1156: * the response to fill, never <code>null</code>.
1157: *
1158: * @throws IOException
1159: * if the WSDL cannot be found in the WAR file.
1160: */
1161: private void handleWsdlRequest(HttpServletResponse response)
1162: throws IOException {
1163: String wsdlLocation = getFileLocation("/WEB-INF/" + _apiName
1164: + ".wsdl");
1165: if (wsdlLocation == null) {
1166: throw new FileNotFoundException("/WEB-INF/" + _apiName
1167: + ".wsdl not found.");
1168: }
1169: InputStream inputXSLT = new URL(wsdlLocation).openStream();
1170: String wsdlText = IOReader.readFully(inputXSLT);
1171:
1172: // Write the text to the output
1173: response.setContentType("text/xml");
1174: response.setStatus(HttpServletResponse.SC_OK);
1175: Writer outputResponse = response.getWriter();
1176: outputResponse.write(wsdlText);
1177: outputResponse.close();
1178: }
1179:
1180: /**
1181: * Handles the request for the _SMD meta function.
1182: *
1183: * @param request
1184: * the request asking for the SMD, never <code>null</code>.
1185: *
1186: * @param response
1187: * the response to fill, never <code>null</code>.
1188: *
1189: * @throws IOException
1190: * if the SMD cannot be created or sent to the output stream.
1191: */
1192: private void handleSmdRequest(HttpServletRequest request,
1193: HttpServletResponse response) throws IOException {
1194: if (_smd == null) {
1195: try {
1196: _smd = createSMD(request);
1197: } catch (Exception ex) {
1198: throw new IOException(ex.getMessage());
1199: }
1200: }
1201:
1202: // Write the text to the output
1203: response
1204: .setContentType(JSONRPCCallingConvention.RESPONSE_CONTENT_TYPE);
1205: response.setStatus(HttpServletResponse.SC_OK);
1206: Writer outputResponse = response.getWriter();
1207: outputResponse.write(_smd);
1208: outputResponse.close();
1209: }
1210:
1211: /**
1212: * Creates the SMD for this API.
1213: * More info at http://dojo.jot.com/SMD and
1214: * http://manual.dojotoolkit.org/WikiHome/DojoDotBook/Book9.
1215: *
1216: * @param request
1217: * the request asking for the SMD, never <code>null</code>.
1218: *
1219: * @return
1220: * the String representation of the SMD JSON Object, never <code>null</code>.
1221: *
1222: * @throws InvalidSpecificationException
1223: * if the specification of the API cannot be found.
1224: *
1225: * @throws EntityNotFoundException
1226: * if the specification of a function cannot be found.
1227: *
1228: * @throws JSONException
1229: * if the JSON object cannot be created correctly.
1230: */
1231: private String createSMD(HttpServletRequest request)
1232: throws InvalidSpecificationException,
1233: EntityNotFoundException, JSONException {
1234: APISpec apiSpec = _api.getAPISpecification();
1235: JSONObject smdObject = new JSONObject();
1236: smdObject.put("SMDVersion", ".1");
1237: smdObject.put("objectName", _api.getName());
1238: smdObject.put("serviceType", "JSON-RPC");
1239: String requestURL = request.getRequestURI();
1240: if (requestURL.indexOf('?') != -1) {
1241: requestURL = requestURL.substring(0, requestURL
1242: .indexOf('?'));
1243: }
1244: smdObject.put("serviceURL", requestURL
1245: + "?_convention=_xins-jsonrpc");
1246: JSONArray methods = new JSONArray();
1247: Iterator itFunctions = _api.getFunctionList().iterator();
1248: while (itFunctions.hasNext()) {
1249: String nextFunction = ((Function) itFunctions.next())
1250: .getName();
1251: JSONObject functionObject = new JSONObject();
1252: functionObject.put("name", nextFunction);
1253: JSONArray inputParameters = new JSONArray();
1254: Map inputParamSpecs = apiSpec.getFunction(nextFunction)
1255: .getInputParameters();
1256: Iterator itParamNames = inputParamSpecs.keySet().iterator();
1257: while (itParamNames.hasNext()) {
1258: String nextParam = (String) itParamNames.next();
1259: JSONObject paramObject = new JSONObject();
1260: paramObject.put("name", nextParam);
1261: inputParameters.put(paramObject);
1262: }
1263: functionObject.put("parameters", inputParameters);
1264: methods.put(functionObject);
1265: }
1266: smdObject.put("methods", methods);
1267: return smdObject.toString();
1268: }
1269: }
|