001: /*
002: * $Id: CallingConvention.java,v 1.105.2.1 2007/09/27 14:43:33 agoubard Exp $
003: *
004: * Copyright 2003-2007 Orange Nederland Breedband B.V.
005: * See the COPYRIGHT file for redistribution and use restrictions.
006: */
007: package org.xins.server;
008:
009: import java.io.IOException;
010: import java.util.ArrayList;
011: import java.util.Arrays;
012: import java.util.Enumeration;
013: import java.util.Iterator;
014:
015: import javax.servlet.http.HttpServletRequest;
016: import javax.servlet.http.HttpServletResponse;
017:
018: import org.xins.common.MandatoryArgumentChecker;
019: import org.xins.common.Utils;
020: import org.xins.common.collections.BasicPropertyReader;
021: import org.xins.common.manageable.Manageable;
022: import org.xins.common.text.ParseException;
023: import org.xins.common.text.TextUtils;
024: import org.xins.common.xml.Element;
025: import org.xins.common.xml.ElementParser;
026:
027: /**
028: * Abstraction of a calling convention. A calling convention determines how an
029: * HTTP request is converted to a XINS function invocation request and how a
030: * XINS function result is converted back to an HTTP response.
031: *
032: * <h2>Thread safety</h2>
033: *
034: * <p>Calling convention implementations must be thread-safe.
035: *
036: * @version $Revision: 1.105.2.1 $ $Date: 2007/09/27 14:43:33 $
037: * @author <a href="mailto:anthony.goubard@japplis.com">Anthony Goubard</a>
038: * @author <a href="mailto:ernst@ernstdehaan.com">Ernst de Haan</a>
039: *
040: * @see CallingConventionManager
041: */
042: abstract class CallingConvention extends Manageable {
043:
044: /**
045: * The default value of the <code>"Server"</code> header sent with an HTTP
046: * response. The actual value is
047: * <code>"XINS/Java Server Framework "</code>, followed by the version of
048: * the framework.
049: *
050: * <p>TODO: Move this constant and the associated functionality elsewhere,
051: * since it does not seem to belong in this class.
052: */
053: private static final String SERVER_HEADER = "XINS/Java Server Framework "
054: + Library.getVersion();
055:
056: /**
057: * The default set of supported HTTP methods.
058: */
059: private static final String[] DEFAULT_SUPPORTED_METHODS = new String[] {
060: "HEAD", "GET", "POST" };
061:
062: /**
063: * The key used in the HttpRequest attribute used to cache the parsed
064: * XML Element when the request is an XML request.
065: */
066: private static final String CACHED_XML_ELEMENT_KEY = "CACHED_XML_ELEMENT_KEY";
067:
068: /**
069: * The current API. The value is set after the construction of the calling
070: * convention.
071: */
072: private API _api;
073:
074: /**
075: * The convention name associated with this calling convention (e.g. _xins-std).
076: */
077: private String _conventionName;
078:
079: /**
080: * Constructs a new <code>CallingConvention</code>. A
081: * <code>CallingConvention</code> instance can only be generated by the
082: * XINS/Java Server Framework.
083: */
084: protected CallingConvention() {
085: }
086:
087: /**
088: * Determines the current API.
089: *
090: * @return
091: * the current {@link API}, never <code>null</code>.
092: *
093: * @since XINS 1.5.0
094: */
095: protected final API getAPI() {
096: return _api;
097: }
098:
099: /**
100: * Sets the current API.
101: *
102: * @param api
103: * the current {@link API}, never <code>null</code>.
104: */
105: final void setAPI(API api) {
106: _api = api;
107: }
108:
109: /**
110: * Gets the name of the convention associated with this CC.
111: *
112: * @return
113: * the name of this calling convention, never <code>null</code>.
114: *
115: * @since XINS 2.1
116: */
117: final String getConventionName() {
118: return _conventionName;
119: }
120:
121: /**
122: * Sets the name of the convention associated with this CC.
123: *
124: * @param conventionName
125: * the calling convention name, never <code>null</code>.
126: *
127: * @since XINS 2.1
128: */
129: final void setConventionName(String conventionName) {
130: _conventionName = conventionName;
131: }
132:
133: /**
134: * Determines which HTTP methods are supported for function invocations.
135: *
136: * <p>Each <code>String</code> in the returned array must be one
137: * supported method.
138: *
139: * <p>The returned array must not be <code>null</code>, it must only
140: * contain valid HTTP method names, so they may not contain whitespace, for
141: * example. Duplicates will be ignored. HTTP method names must be in uppercase.
142: *
143: * <p>There must be at least one HTTP method supported for function
144: * invocations.
145: *
146: * <p>Note that <em>OPTIONS</em> must not be returned by this method, as it
147: * is not an HTTP method that can ever be used to invoke a XINS function.
148: * <p>HTTP <em>OPTIONS</em> requests are treated differently. For the path
149: * <code>*</code> the capabilities of the whole server are returned. For other
150: * paths, the appropriate calling convention is determined, after which the
151: * set of supported HTTP methods is returned to the called.
152: *
153: * @return
154: * the HTTP methods supported, in a <code>String</code> array, must
155: * not be <code>null</code>.
156: *
157: * @since XINS 1.5.0
158: */
159: protected String[] getSupportedMethods() {
160: return DEFAULT_SUPPORTED_METHODS;
161: }
162:
163: /**
164: * Determines which HTTP methods are supported for function invocations,
165: * for the specified request.
166: *
167: * <p>Each <code>String</code> in the returned array must be one
168: * supported method.
169: *
170: * <p>The returned array may be <code>null</code>. If it is not, then the
171: * returned array must only contain valid HTTP method names, so they may
172: * not contain whitespace, for example. HTTP method names must be in uppercase.
173: *
174: * <p>There must be at least one HTTP method supported for function
175: * invocations.
176: *
177: * <p>Note that <em>OPTIONS</em> must not be returned by this method, as it
178: * is not an HTTP method that can ever be used to invoke a XINS function.
179: *
180: * <p>The set of supported methods must be a subset of the set returned by
181: * {@link #getSupportedMethods()}.
182: *
183: * <p>The default implementation of this method returns the set returned by
184: * {@link #getSupportedMethods()}.
185: *
186: * @param request
187: * the request to determine the supported methods for.
188: *
189: * @return
190: * the HTTP methods supported for the specified request, in a
191: * <code>String</code> array, can be <code>null</code>.
192: *
193: * @since XINS 1.5.0
194: */
195: protected String[] getSupportedMethods(HttpServletRequest request) {
196: return getSupportedMethods();
197: }
198:
199: /**
200: * Checks if the specified request can be handled by this calling
201: * convention. Assuming this <code>CallingConvention</code> instance is
202: * usable and the HTTP method is supported, this method delegates to
203: * {@link #matches(HttpServletRequest)}.
204: *
205: * <p>If this calling convention is not usable (see {@link #isUsable()}),
206: * then <code>false</code> is returned, even <em>before</em> calling
207: * {@link #matches(HttpServletRequest)}.
208: *
209: * <p>If this method does not support the HTTP method for function
210: * invocations, then <code>false</code> is returned.
211: *
212: * <p>If {@link #matches(HttpServletRequest)} throws an exception, then
213: * this exception is ignored and <code>false</code> is returned.
214: *
215: * <p>This method is guaranteed not to throw any exception.
216: *
217: * @param httpRequest
218: * the HTTP request to investigate, cannot be <code>null</code>.
219: *
220: * @return
221: * <code>true</code> if this calling convention is <em>possibly</em>
222: * able to handle this request, or <code>false</code> if it is
223: * <em>definitely</em> not able to handle this request.
224: */
225: final boolean matchesRequest(HttpServletRequest httpRequest) {
226:
227: // First check if this CallingConvention instance is bootstrapped and
228: // initialized
229: if (!isUsable()) {
230: return false;
231: }
232:
233: // Make sure the HTTP method is supported
234: String method = httpRequest.getMethod();
235: if (!Arrays.asList(getSupportedMethods(httpRequest)).contains(
236: method)
237: && !"OPTIONS".equals(method)) {
238: return false;
239: }
240:
241: // Delegate to the 'matches' method
242: try {
243: return matches(httpRequest);
244:
245: // Assume that an exception indicates the request cannot be handled
246: //
247: // NOTE: We do not log this exception, because it would possibly show up
248: // in the logs on a regular basis, drawing attention to a
249: // non-issue.
250: } catch (Throwable exception) {
251: return false;
252: }
253: }
254:
255: /**
256: * Checks if the specified request can possibly be handled by this calling
257: * convention as a function invocation.
258: *
259: * <p>Implementations of this method should be optimized for performance,
260: * as this method may be called for each incoming request. Also, this
261: * method should not have any side-effects except possibly some caching in
262: * case there is a match.
263: *
264: * <p>If this method throws any exception, the exception is logged as an
265: * ignorable exception and <code>false</code> is assumed.
266: *
267: * <p>This method should only be called by the XINS/Java Server Framework.
268: *
269: * @param httpRequest
270: * the HTTP request to investigate, never <code>null</code>.
271: *
272: * @return
273: * <code>true</code> if this calling convention is <em>possibly</em>
274: * able to handle this request, or <code>false</code> if it is
275: * <em>definitely</em> not able to handle this request.
276: *
277: * @throws Exception
278: * if analysis of the request causes an exception; in this case
279: * <code>false</code> will be assumed by the framework.
280: *
281: * @since XINS 1.4.0
282: */
283: protected abstract boolean matches(HttpServletRequest httpRequest)
284: throws Exception;
285:
286: /**
287: * Converts an HTTP request to a XINS request (wrapper method). This method
288: * checks the arguments, checks that the HTTP method is actually supported,
289: * calls the implementation method and then checks the return value from
290: * that method.
291: *
292: * @param httpRequest
293: * the HTTP request, cannot be <code>null</code>.
294: *
295: * @return
296: * the XINS request object, never <code>null</code>.
297: *
298: * @throws IllegalStateException
299: * if this calling convention is currently not usable, see
300: * {@link Manageable#assertUsable()}.
301: *
302: * @throws IllegalArgumentException
303: * if <code>httpRequest == null</code>.
304: *
305: * @throws InvalidRequestException
306: * if the request is considerd to be invalid, at least for this calling
307: * convention; either because the HTTP method is not supported, or
308: * because {@link #convertRequestImpl(HttpServletRequest)} indicates so.
309: *
310: * @throws FunctionNotSpecifiedException
311: * if the request does not indicate the name of the function to execute.
312: */
313: final FunctionRequest convertRequest(HttpServletRequest httpRequest)
314: throws IllegalStateException, IllegalArgumentException,
315: InvalidRequestException, FunctionNotSpecifiedException {
316:
317: // Make sure the current state is okay
318: assertUsable();
319:
320: // Check preconditions
321: MandatoryArgumentChecker.check("httpRequest", httpRequest);
322:
323: // Delegate to the implementation method
324: FunctionRequest xinsRequest;
325: try {
326: xinsRequest = convertRequestImpl(httpRequest);
327:
328: // Filter any thrown exceptions
329: } catch (Throwable exception) {
330: if (exception instanceof InvalidRequestException) {
331: throw (InvalidRequestException) exception;
332: } else if (exception instanceof FunctionNotSpecifiedException) {
333: throw (FunctionNotSpecifiedException) exception;
334: } else {
335: throw Utils.logProgrammingError(exception);
336: }
337: }
338:
339: // Make sure the returned value is not null
340: if (xinsRequest == null) {
341: throw Utils.logProgrammingError("Method returned null.");
342: }
343:
344: return xinsRequest;
345: }
346:
347: /**
348: * Converts an HTTP request to a XINS request (implementation method). This
349: * method should only be called from the XINS/Java Server Framework self.
350: * Then it is guaranteed that:
351: * <ul>
352: * <li>the state is usable;
353: * <li>the <code>httpRequest</code> argument is not <code>null</code>;
354: * <li>the HTTP method is in the set of supported methods, as indicated
355: * by {@link #getSupportedMethods()}.
356: * </ul>
357: *
358: * <p>Note that {@link #getSupportedMethods(HttpServletRequest)} will not
359: * have been called prior to this method call.
360: *
361: * @param httpRequest
362: * the HTTP request.
363: *
364: * @return
365: * the XINS request object, should not be <code>null</code>.
366: *
367: * @throws InvalidRequestException
368: * if the request is considerd to be invalid.
369: *
370: * @throws FunctionNotSpecifiedException
371: * if the request does not indicate the name of the function to execute.
372: */
373: protected abstract FunctionRequest convertRequestImpl(
374: HttpServletRequest httpRequest)
375: throws InvalidRequestException,
376: FunctionNotSpecifiedException;
377:
378: /**
379: * Converts a XINS result to an HTTP response (wrapper method). This method
380: * checks the arguments, then calls the implementation method and then
381: * checks the return value from that method.
382: *
383: * <p>Note that this method is not called if there is an error while
384: * converting the request.
385: *
386: * @param xinsResult
387: * the XINS result object that should be converted to an HTTP response,
388: * cannot be <code>null</code>.
389: *
390: * @param httpResponse
391: * the HTTP response object to configure, cannot be <code>null</code>.
392: *
393: * @param httpRequest
394: * the HTTP request, cannot be <code>null</code>.
395: *
396: * @throws IllegalStateException
397: * if this calling convention is currently not usable, see
398: * {@link Manageable#assertUsable()}.
399: *
400: * @throws IllegalArgumentException
401: * if <code>xinsResult == null
402: * || httpResponse == null
403: * || httpRequest == null</code>.
404: *
405: * @throws IOException
406: * if the invocation of any of the methods in either
407: * <code>httpResponse</code> or <code>httpRequest</code> caused an I/O
408: * error.
409: */
410: final void convertResult(FunctionResult xinsResult,
411: HttpServletResponse httpResponse,
412: HttpServletRequest httpRequest)
413: throws IllegalStateException, IllegalArgumentException,
414: IOException {
415:
416: // Make sure the current state is okay
417: assertUsable();
418:
419: // Check preconditions
420: MandatoryArgumentChecker.check("xinsResult", xinsResult,
421: "httpResponse", httpResponse, "httpRequest",
422: httpRequest);
423:
424: // By default, all calling conventions return the same "Server" header.
425: // This can be overridden in the convertResultImpl() method.
426: httpResponse.addHeader("Server", SERVER_HEADER);
427:
428: // Delegate to the implementation method
429: try {
430: convertResultImpl(xinsResult, httpResponse, httpRequest);
431:
432: // Filter any thrown exceptions
433: } catch (Throwable exception) {
434: if (exception instanceof IOException) {
435: Log.log_3506(exception, getClass().getName());
436: throw (IOException) exception;
437: } else {
438: throw Utils.logProgrammingError(exception);
439: }
440: }
441: }
442:
443: /**
444: * Converts a XINS result to an HTTP response (implementation method). This
445: * method should only be called from the XINS/Java Server Framework self.
446: * Then it is guaranteed that none of the arguments is <code>null</code>.
447: *
448: * @param xinsResult
449: * the XINS result object that should be converted to an HTTP response,
450: * will not be <code>null</code>.
451: *
452: * @param httpResponse
453: * the HTTP response object to configure.
454: *
455: * @param httpRequest
456: * the HTTP request.
457: *
458: * @throws IOException
459: * if the invocation of any of the methods in either
460: * <code>httpResponse</code> or <code>httpRequest</code> caused an I/O
461: * error.
462: */
463: protected abstract void convertResultImpl(
464: FunctionResult xinsResult,
465: HttpServletResponse httpResponse,
466: HttpServletRequest httpRequest) throws IOException;
467:
468: // XXX: Replace IOException with more appropriate exception?
469:
470: /**
471: * Parses XML from the specified HTTP request and checks that the content
472: * type is correct.
473: *
474: * <p>This method uses a cache to optimize performance if either of the
475: * <code>parseXMLRequest</code> methods is called multiple times for the
476: * same request.
477: *
478: * <p>Calling this method is equivalent with calling
479: * {@link #parseXMLRequest(HttpServletRequest,boolean)} with the
480: * <code>checkType</code> argument set to <code>true</code>.
481: *
482: * @param httpRequest
483: * the HTTP request, cannot be <code>null</code>.
484: *
485: * @return
486: * the parsed element, never <code>null</code>.
487: *
488: * @throws IllegalArgumentException
489: * if <code>httpRequest == null</code>.
490: *
491: * @throws InvalidRequestException
492: * if the HTTP request cannot be read or cannot be parsed correctly.
493: *
494: * @since XINS 1.4.0
495: */
496: protected Element parseXMLRequest(HttpServletRequest httpRequest)
497: throws IllegalArgumentException, InvalidRequestException {
498: return parseXMLRequest(httpRequest, true);
499: }
500:
501: /**
502: * Parses XML from the specified HTTP request and optionally checks that
503: * the content type is correct.
504: *
505: * <p>Since XINS 1.4.0, this method uses a cache to optimize performance if
506: * either of the <code>parseXMLRequest</code> methods is called multiple
507: * times for the same request.
508: *
509: * @param httpRequest
510: * the HTTP request, cannot be <code>null</code>.
511: *
512: * @param checkType
513: * flag indicating whether this method should check that the content
514: * type of the request is <em>text/xml</em>.
515: *
516: * @return
517: * the parsed element, never <code>null</code>.
518: *
519: * @throws IllegalArgumentException
520: * if <code>httpRequest == null</code>.
521: *
522: * @throws InvalidRequestException
523: * if the HTTP request cannot be read or cannot be parsed correctly.
524: *
525: * @since XINS 1.3.0
526: */
527: protected Element parseXMLRequest(HttpServletRequest httpRequest,
528: boolean checkType) throws IllegalArgumentException,
529: InvalidRequestException {
530:
531: // Check arguments
532: MandatoryArgumentChecker.check("httpRequest", httpRequest);
533:
534: // Determine if the request matches the cached request and the parsed
535: // XML is already cached
536: Object cached = httpRequest
537: .getAttribute(CACHED_XML_ELEMENT_KEY);
538:
539: // Cache miss
540: if (cached == null) {
541: Log.log_3512();
542:
543: // Cache hit
544: } else {
545: Log.log_3513();
546: return (Element) cached;
547: }
548:
549: // Always first check the content type, even if checking is enabled. We
550: // do this because the parsed request will only be stored if the content
551: // type was OK.
552: String contentType = httpRequest.getContentType();
553: String errorMessage = null;
554: if (contentType == null || contentType.trim().length() < 1) {
555: errorMessage = "No content type set.";
556: } else {
557: String contentTypeLC = contentType.toLowerCase();
558: if (!("text/xml".equals(contentTypeLC) || contentTypeLC
559: .startsWith("text/xml;"))) {
560: errorMessage = "Invalid content type \""
561: + contentType
562: + "\". Expected \"text/xml\" (case-insensitive) or a variant of it.";
563: }
564: }
565:
566: // The content-type check was unsuccessful
567: if (errorMessage != null) {
568:
569: // Log: Not caching XML since the content type is not "text/xml"
570: Log.log_3515();
571:
572: // If checking is enabled
573: if (checkType) {
574: throw new InvalidRequestException(errorMessage);
575: }
576: }
577:
578: // Parse the content in the HTTP request
579: ElementParser parser = new ElementParser();
580: Element element;
581: try {
582: element = parser.parse(httpRequest.getReader());
583:
584: // I/O error
585: } catch (IOException ex) {
586: String message = "Failed to read XML request.";
587: throw new InvalidRequestException(message, ex);
588:
589: // Parsing error
590: } catch (ParseException ex) {
591: String message = "Failed to parse XML request.";
592: throw new InvalidRequestException(message, ex);
593: }
594:
595: // Only store in the cache if the content type was OK
596: if (errorMessage == null) {
597: httpRequest.setAttribute(CACHED_XML_ELEMENT_KEY, element);
598: Log.log_3514();
599: }
600:
601: return element;
602: }
603:
604: /**
605: * Gathers all parameters from the specified request. The parameters are
606: * returned as a {@link BasicPropertyReader}.
607: * If no parameters are found, then <code>null</code> is returned.
608: *
609: * <p>If a parameter is found to have multiple values, then an
610: * {@link InvalidRequestException} is thrown.
611: *
612: * @param httpRequest
613: * the HTTP request to get the parameters from, cannot be
614: * <code>null</code>.
615: *
616: * @return
617: * the properties found, or <code>null</code> if none were found.
618: *
619: * @throws InvalidRequestException
620: * if a parameter is found that has multiple values.
621: */
622: BasicPropertyReader gatherParams(HttpServletRequest httpRequest)
623: throws InvalidRequestException {
624:
625: // Get the parameters from the HTTP request
626: Enumeration params = httpRequest.getParameterNames();
627:
628: // The property set to return from this method
629: BasicPropertyReader pr;
630:
631: // If there are no parameters, then return null
632: if (!params.hasMoreElements()) {
633: pr = null;
634:
635: // There seem to be some parameters
636: } else {
637: pr = new BasicPropertyReader();
638:
639: do {
640: // Get the parameter name
641: String name = (String) params.nextElement();
642:
643: // Get all parameter values (can be multiple)
644: String[] values = httpRequest.getParameterValues(name);
645:
646: // Be gentle, allow nulls and zero-sized arrays
647: if (values != null && values.length != 0) {
648:
649: // Get the parameter value, allowing duplicate values, but not
650: // different ones; this may throw an InvalidRequestException
651: String value = getParamValue(name, values);
652:
653: // Associate the name with the one and only value
654: pr.set(name, value);
655: }
656: } while (params.hasMoreElements());
657: }
658: return pr;
659: }
660:
661: /**
662: * Changes a parameter set to remove all parameters that should not be
663: * passed to functions.
664: *
665: * <p>A parameter will be removed if it matches any of the following
666: * conditions:
667: *
668: * <ul>
669: * <li>parameter name is <code>null</code>;
670: * <li>parameter name is empty;
671: * <li>parameter value is <code>null</code>;
672: * <li>parameter value is empty;
673: * <li>parameter name equals <code>"function"</code>.
674: * </ul>
675: *
676: * @param parameters
677: * the {@link BasicPropertyReader} containing the set of parameters
678: * to investigate, cannot be <code>null</code>.
679: *
680: * @throws IllegalArgumentException
681: * if <code>parameters == null</code>.
682: */
683: static void cleanUpParameters(BasicPropertyReader parameters)
684: throws IllegalArgumentException {
685:
686: // Check arguments
687: MandatoryArgumentChecker.check("parameters", parameters);
688:
689: // Get the parameter names
690: Iterator names = parameters.getNames();
691:
692: // Loop through all parameters
693: ArrayList toRemove = new ArrayList();
694: while (names.hasNext()) {
695:
696: // Determine parameter name and value
697: String name = (String) names.next();
698: String value = parameters.get(name);
699:
700: // If the parameter name or value is empty, or if the name is
701: // "function", then mark the parameter as 'to be removed'.
702: // Parameters starting with an underscore are reserved for XINS, so
703: // mark these as 'to be removed' as well.
704: if (TextUtils.isEmpty(name) || TextUtils.isEmpty(value)
705: || "function".equals(name) || name.charAt(0) == '_') {
706: toRemove.add(name);
707: }
708: }
709:
710: // If there is anything to remove, then do so
711: Iterator itRemove = toRemove.iterator();
712: while (itRemove.hasNext()) {
713: String name = (String) itRemove.next();
714: parameters.set(name, null);
715: }
716: }
717:
718: /**
719: * Determines a single value for a parameter based on an array of values.
720: * If there is only one value, then that value is returned. If there are
721: * multiple equal values, then the value is returned as well. However, if
722: * there are multiple values and at least one of them is different, then an
723: * {@link InvalidRequestException} is thrown.
724: *
725: * @param name
726: * the name of the parameter, only used when throwing an
727: * {@link InvalidRequestException}, should not be <code>null</code>.
728: *
729: * @param values
730: * the values, should not be <code>null</code> and should not have a
731: * size of zero.
732: *
733: * @return
734: * the single value of the parameter, if any.
735: *
736: * @throws NullPointerException
737: * if <code>values == null || values[<em>n</em>] == null</code>, where
738: * <code>0 <= <em>n</em> < values.length</code>.
739: *
740: * @throws IndexOutOfBoundsException
741: * if <code>values.length < 1</code>.
742: *
743: * @throws InvalidRequestException
744: * if the parameter is found to have multiple different values.
745: */
746: private final String getParamValue(String name, String[] values)
747: throws NullPointerException, IndexOutOfBoundsException,
748: InvalidRequestException {
749:
750: String value = values[0];
751:
752: // We only need to do crunching if there is more than one value
753: if (values.length > 1) {
754: for (int i = 1; i < values.length; i++) {
755: String other = values[i];
756: if (!value.equals(other)) {
757: throw new InvalidRequestException(
758: "Found multiple values for the parameter named \""
759: + name + "\".");
760: }
761: }
762: }
763:
764: return value;
765: }
766: }
|