001: package org.gomba;
002:
003: import java.io.IOException;
004: import java.sql.SQLException;
005: import java.text.SimpleDateFormat;
006: import java.util.ArrayList;
007: import java.util.Collections;
008: import java.util.Date;
009: import java.util.HashMap;
010: import java.util.Iterator;
011: import java.util.List;
012: import java.util.Map;
013: import java.util.regex.Matcher;
014: import java.util.regex.Pattern;
015:
016: import javax.servlet.ServletConfig;
017: import javax.servlet.ServletException;
018: import javax.servlet.http.HttpServlet;
019: import javax.servlet.http.HttpServletRequest;
020: import javax.servlet.http.HttpServletResponse;
021: import javax.sql.DataSource;
022: import javax.xml.transform.OutputKeys;
023: import javax.xml.transform.Result;
024: import javax.xml.transform.Source;
025: import javax.xml.transform.Transformer;
026: import javax.xml.transform.TransformerException;
027: import javax.xml.transform.TransformerFactory;
028: import javax.xml.transform.sax.SAXSource;
029: import javax.xml.transform.stream.StreamResult;
030:
031: import org.gomba.utils.servlet.ServletLogger;
032: import org.gomba.utils.token.IdGenerator;
033: import org.gomba.utils.xml.ContentHandlerUtils;
034: import org.gomba.utils.xml.ObjectInputSource;
035: import org.gomba.utils.xml.ObjectXMLReader;
036: import org.xml.sax.InputSource;
037: import org.xml.sax.SAXException;
038:
039: /**
040: * This servlet implements transactions for RESTful web services. This servlet
041: * is used to start, commit or rollback a transaction. This servlet will be
042: * typically mapped to <code>/transactions/*</code>.
043: *
044: * HTTP methods are mapped to transaction operations in this way:
045: * <dl>
046: * <dt>POST</dt>
047: * <dd>Start a new transaction. Posting an empty resource will create a new
048: * transaction. In the future we may add an XML document type to specify
049: * transaction options.</dd>
050: * <dt>GET</dt>
051: * <dd>Get information about a transaction.</dd>
052: * <dt>PUT</dt>
053: * <dd>Commit a transaction. Putting an empty resource is allowed. In the
054: * future we may support PUTting the (updated) XML data returned by GET.</dd>
055: * <dt>DELETE</dt>
056: * <dd>Rollback a transaction and destroy it.</dd>
057: * </dl>
058: *
059: * The XML returned by this servlet in response to GET request looks like this:
060: *
061: * <pre>
062: * <transaction>
063: * <uri>http://domain.com/transactions/238476245jhg34589345k</uri>
064: * <creationTime>2004-12-25T00:00:00</creationTime>
065: * <lastAccessedTime>2004-12-25T00:10:00</lastAccessedTime>
066: * <maxInactiveInterval>600</maxInactiveInterval>
067: * </transaction>
068: * </pre>
069: *
070: * <p>
071: * TODO: provide a DTD.
072: * </p>
073: *
074: * Init-params:
075: * <dl>
076: * <dt>transaction-id</dt>
077: * <dd>An expression that evaluates to the transaction id. May contain ${}
078: * parameters. This will typically be: ${path.0} (Required)</dd>
079: * <dt>transaction-uri</dt>
080: * <dd>The transaction URI. This is not a full-blown expression, ${} cannot be
081: * used here. Only ${transaction.id} will be replaced with actual transaction
082: * id. A typical setting is: http://domain.org/transactions/${transaction.id}.
083: * In a sense, this the reverse of the transaction-id expression: transaction-id
084: * is used to extract an id from a URI, while transaction-URI is used to
085: * generate a URI from an id. (Required)</dd>
086: * <dt>transaction-timeout</dt>
087: * <dd>The transaction timeout interval for all transactions created by this
088: * servlet. The specified timeout must be expressed in a whole number of
089: * seconds. Since version 0.8 this value may be exceeded, it depends on when the
090: * HarvesterThread processes the transaction. The default value is 30.
091: * (Optional)</dd>
092: * </dt>
093: * <dt>jvm-route</dt>
094: * <dd>Identifier which must be used in load balancing scenarios to enable
095: * session affinity. The identifier, which must be unique across all servers
096: * which participate in the cluster, will be appended to the generated
097: * transaction identifier, therefore allowing the front end proxy to always
098: * forward requests that belong to a particular transaction to the same
099: * instance. Value can be an expression evaluated at servlet initialization
100: * time, so don't use request-related expressions. The "systemProperty"
101: * parameter domain is particurarly useful for this setting.</dd>
102: * </dl>
103: *
104: * @author Flavio Tordini
105: * @version $Id: TransactionServlet.java,v 1.8 2005/12/07 11:19:01 flaviotordini
106: * Exp $
107: * @see http://www.seairth.com/web/resttp.html,
108: * http://www.xml.com/lpt/a/2004/08/11/rest.html,
109: * http://lists.xml.org/archives/xml-dev/200402/msg00267.html
110: * http://groups.yahoo.com/group/rest-discuss/message/4141
111: */
112: public class TransactionServlet extends HttpServlet {
113:
114: /**
115: * A regular expression used to build the transaction URI.
116: */
117: private final static Pattern TRANSACTION_ID_PATTERN = Pattern
118: .compile("\\$\\{transaction.id\\}");
119:
120: /**
121: * Name of the context attribute that holds the transactions Map. Mapping is
122: * transaction URI to Trasaction.
123: */
124: protected final static String CONTEXT_ATTRIBUTE_NAME_TRANSACTIONS = "org_gomba_transactions";
125:
126: /**
127: * Name of the context attribute that holds the servlet instance.
128: */
129: protected final static String CONTEXT_ATTRIBUTE_NAME_SERVLET = "org_gomba_transactionServlet";
130:
131: private final static String INIT_PARAM_TRANSACTION_URI = "transaction-uri";
132:
133: private final static String INIT_PARAM_TRANSACTION_ID = "transaction-id";
134:
135: private final static String INIT_PARAM_TRANSACTION_TIMEOUT = "transaction-timeout";
136:
137: private final static String INIT_PARAM_JVMROUTE = "jvm-route";
138:
139: private final static int DEFAULT_TRANSACTION_TIMEOUT = 30;
140:
141: /**
142: * <code>true</code> if debug logging is turned on.
143: */
144: private boolean debugMode;
145:
146: /**
147: * A logger to be passed around to enable servlet logging in non-servlet
148: * classes.
149: */
150: private ServletLogger logger;
151:
152: /**
153: * The data source to query.
154: */
155: private DataSource dataSource;
156:
157: /**
158: * Expression that evaluates to the transaction id. Will tipically be the
159: * first extra path element. The id of a transaction is private affair of
160: * this servlet. Clients refer to a transaction using its URI.
161: */
162: private Expression idExpression;
163:
164: /**
165: * Pseudo-expression that evaluates to the transaction URI. This is not a
166: * full-blown expression, only the <code>TRANSACTION_ID_PATTERN</code> is
167: * replaced with actual transaction id.
168: */
169: private String uriExpression;
170:
171: /**
172: * Transaction timeout in seconds
173: */
174: private int transactionTimeout = DEFAULT_TRANSACTION_TIMEOUT;
175:
176: /**
177: * Map containing current active transactions. Mapping is transaction URI to
178: * Transaction.
179: */
180: private Map transactions;
181:
182: /**
183: * This will help us generate secure random transaction ids
184: */
185: private final IdGenerator idGenerator = new IdGenerator();
186:
187: /**
188: * <code>true</code> when the destroy() method has been called by the
189: * servlet container.
190: */
191: private boolean destroyed;
192:
193: /**
194: * Dummy object acting as a semaphore for the harvester thread.
195: */
196: private final Object semaphore = new Object();
197:
198: /**
199: * Server identifier
200: */
201: private String jvmRoute;
202:
203: /**
204: * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
205: */
206: public void init(ServletConfig config) throws ServletException {
207: super .init(config);
208:
209: // debug mode
210: this .debugMode = AbstractServlet.getDebugMode(config);
211:
212: // build our strange logger
213: this .logger = new ServletLogger(this , this .debugMode);
214:
215: // get the JNDI data source
216: this .dataSource = AbstractServlet.getDataSource(config);
217:
218: // transaction URI pseudo-expression
219: this .uriExpression = config
220: .getInitParameter(INIT_PARAM_TRANSACTION_URI);
221: if (this .uriExpression == null) {
222: throw new ServletException("Missing init-param: "
223: + INIT_PARAM_TRANSACTION_URI);
224: }
225:
226: // transaction id expression
227: String idExpressionStr = config
228: .getInitParameter(INIT_PARAM_TRANSACTION_ID);
229: if (idExpressionStr == null) {
230: throw new ServletException("Missing init-param: "
231: + INIT_PARAM_TRANSACTION_ID);
232: }
233: try {
234: this .idExpression = new Expression(idExpressionStr);
235: } catch (Exception e) {
236: throw new ServletException("Error parsing "
237: + INIT_PARAM_TRANSACTION_ID + " expression.", e);
238: }
239:
240: // transaction timeout
241: String transactionTimeoutStr = config
242: .getInitParameter(INIT_PARAM_TRANSACTION_TIMEOUT);
243: if (transactionTimeoutStr != null) {
244: this .transactionTimeout = Integer
245: .parseInt(transactionTimeoutStr);
246: }
247:
248: // server identifier
249: String jvmRouteStr = config
250: .getInitParameter(INIT_PARAM_JVMROUTE);
251: if (jvmRouteStr != null) {
252: Expression jvmRouteExpression;
253: try {
254: jvmRouteExpression = new Expression(jvmRouteStr);
255: } catch (Exception e) {
256: throw new ServletException("Error parsing "
257: + INIT_PARAM_JVMROUTE + " expression.", e);
258: }
259: // domains using the request will throw a npe
260: ParameterResolver parameterResolver = new ParameterResolver(
261: null);
262: try {
263: this .jvmRoute = jvmRouteExpression.replaceParameters(
264: parameterResolver).toString();
265: } catch (Exception e) {
266: throw new ServletException("Error evaluating "
267: + INIT_PARAM_JVMROUTE + " expression.", e);
268: }
269: }
270:
271: // put this servlet instance in application scope
272: // FIXME servlets shold not be put in scopes
273: if (getServletContext().getAttribute(
274: CONTEXT_ATTRIBUTE_NAME_SERVLET) != null) {
275: throw new ServletException("A " + this .getClass().getName()
276: + " is already configured for the current context.");
277: }
278: getServletContext().setAttribute(
279: CONTEXT_ATTRIBUTE_NAME_SERVLET, this );
280:
281: // init the transactions map
282: // since we're in a servlet env where multiple thread will modify the
283: // map, we need a synchronized impl
284: this .transactions = Collections.synchronizedMap(new HashMap());
285: getServletContext().setAttribute(
286: CONTEXT_ATTRIBUTE_NAME_TRANSACTIONS, this .transactions);
287:
288: // start background thread that removes expired transactions
289: TransactionHarvester harvester = new TransactionHarvester();
290: harvester.setDaemon(true);
291: harvester.setPriority(Thread.MIN_PRIORITY);
292: harvester.start();
293:
294: }
295:
296: /**
297: * Create a new transaction and add it the map.
298: */
299: protected Transaction createTransaction() throws ServletException {
300:
301: final Transaction transaction;
302:
303: // build transaction URI
304: // since this block is not synchronized
305: // there is a slight chance to generate an existing id
306: String transactionURI;
307: do {
308: // generate id
309: String transactionId = this .idGenerator.generateId();
310: if (this .jvmRoute != null) {
311: transactionId += '.' + this .jvmRoute;
312: }
313: transactionURI = getTransactionURI(transactionId);
314: } while (this .transactions.containsKey(transactionURI));
315:
316: // create transaction
317: transaction = new Transaction(this .logger, this .dataSource,
318: transactionURI, this .transactionTimeout);
319:
320: // add transaction to our map
321: this .transactions.put(transaction.getUri(), transaction);
322:
323: this .logger.debug("Created transaction: "
324: + transaction.getUri());
325:
326: return transaction;
327: }
328:
329: /**
330: * Create a transaction.
331: *
332: * @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest,
333: * javax.servlet.http.HttpServletResponse)
334: */
335: protected void doPost(HttpServletRequest request,
336: HttpServletResponse response) throws ServletException,
337: IOException {
338:
339: // TODO do we need XML from the request body ???
340: // TODO if we need it, it should be customizable with XSLT
341:
342: // create the transaction
343: Transaction transaction = createTransaction();
344:
345: // 201!
346: response.setStatus(HttpServletResponse.SC_CREATED);
347:
348: // set Location header
349: response.setHeader("Location", transaction.getUri());
350:
351: // TODO output XML with XLink
352: // TODO and make it customizable via XSLT
353:
354: }
355:
356: /**
357: * Get the transaction id by evaluating the id expression.
358: *
359: * @return Never returns null
360: */
361: private String getTransactionId(ParameterResolver parameterResolver)
362: throws ServletException {
363:
364: Object obj;
365: try {
366: obj = this .idExpression
367: .replaceParameters(parameterResolver);
368: } catch (Exception e) {
369: throw new ServletException(
370: "Error evaluating transaction id expression.", e);
371: }
372:
373: if (obj instanceof java.lang.String) {
374: return (String) obj;
375: }
376:
377: throw new ServletException(
378: "Transaction id expression does not evaluate to String: "
379: + obj.getClass().getName());
380:
381: }
382:
383: /**
384: * Get the transaction URI by evaluating the URI pseudo expression.
385: */
386: private String getTransactionURI(String transactionId) {
387: Matcher m = TRANSACTION_ID_PATTERN.matcher(this .uriExpression);
388: return m.replaceFirst(transactionId);
389: }
390:
391: /**
392: * Get the Transaction object for the specified request.
393: *
394: * @return May return null if the specified transaction does not exist.
395: */
396: private Transaction getTransaction(HttpServletRequest request)
397: throws ServletException {
398:
399: // create the parameter resolver that will help us throughout this
400: // request
401: final ParameterResolver parameterResolver = new ParameterResolver(
402: request);
403:
404: // get transaction id
405: String transactionId = getTransactionId(parameterResolver);
406:
407: // get transaction URI
408: String transactionURI = getTransactionURI(transactionId);
409:
410: // get transaction
411: Transaction transaction = (Transaction) this .transactions
412: .get(transactionURI);
413:
414: if (transaction == null) {
415: log("Invalid or expired transaction: " + transactionURI);
416: }
417:
418: return transaction;
419:
420: }
421:
422: /**
423: * Get the Transaction object for the specified request.
424: *
425: * @return May return null if the specified transaction does not exist.
426: */
427: private Transaction getAndRemoveTransaction(
428: HttpServletRequest request) throws ServletException {
429:
430: // create the parameter resolver that will help us throughout this
431: // request
432: final ParameterResolver parameterResolver = new ParameterResolver(
433: request);
434:
435: // get transaction id
436: String transactionId = getTransactionId(parameterResolver);
437:
438: // get transaction URI
439: String transactionURI = getTransactionURI(transactionId);
440:
441: // get transaction
442: Transaction transaction = (Transaction) this .transactions
443: .remove(transactionURI);
444:
445: if (transaction == null) {
446: log("Invalid, expired or completed transaction: "
447: + transactionURI);
448: }
449:
450: return transaction;
451:
452: }
453:
454: /**
455: * Obtain information about a transaction.
456: *
457: * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest,
458: * javax.servlet.http.HttpServletResponse)
459: */
460: protected void doGet(HttpServletRequest request,
461: HttpServletResponse response) throws ServletException,
462: IOException {
463:
464: // get transaction
465: Transaction transaction = getTransaction(request);
466: if (transaction == null) {
467: response.sendError(HttpServletResponse.SC_NOT_FOUND);
468: return;
469: }
470:
471: // output xml
472: try {
473: serializeXML(transaction, response);
474: } catch (Exception e) {
475: throw new ServletException(
476: "Error serializing transaction to XML.", e);
477: }
478:
479: }
480:
481: /**
482: * Remove a transaction from the map.
483: */
484: private void removeTransaction(Transaction transaction)
485: throws ServletException {
486: Object previousValue = this .transactions.remove(transaction
487: .getUri());
488: if (previousValue == null) {
489: log("Transaction already removed: " + transaction.getUri());
490: }
491: }
492:
493: /**
494: * Serialize an object to XML using SAX and TrAX APIs in a smart way.
495: * (dagnele sucks :)
496: *
497: * @param object
498: * The object to serialize
499: * @param saxReader
500: * The SAX "parser"
501: * @param response
502: * The HTTP response
503: * @see <a
504: * href="http://java.sun.com/j2se/1.4.2/docs/api/javax/xml/transform/package-summary.html">TrAX
505: * API </a>
506: */
507: private void serializeXML(Transaction object,
508: HttpServletResponse response) throws TransformerException,
509: IOException {
510:
511: ObjectXMLReader saxReader = new TransactionXMLReader();
512:
513: // Let the HTTP client know the output content type
514: response.setContentType("text/xml");
515:
516: // Create TrAX Transformer
517: Transformer t = TransformerFactory.newInstance()
518: .newTransformer();
519:
520: // Set trasformation output properties
521: t.setOutputProperty(OutputKeys.ENCODING, response
522: .getCharacterEncoding());
523:
524: // Create the trasformation source using our custom ObjectInputSource
525: InputSource inputSource = new ObjectInputSource(object);
526: Source source = new SAXSource(saxReader, inputSource);
527:
528: // Create the trasformation result
529: // Result result = new StreamResult(response.getWriter());
530: Result result = new StreamResult(response.getOutputStream());
531:
532: // Go!
533: t.transform(source, result);
534:
535: }
536:
537: /**
538: * Commit a transaction.
539: *
540: * @see javax.servlet.http.HttpServlet#doPut(javax.servlet.http.HttpServletRequest,
541: * javax.servlet.http.HttpServletResponse)
542: */
543: protected void doPut(HttpServletRequest request,
544: HttpServletResponse response) throws ServletException,
545: IOException {
546:
547: // get transaction
548: Transaction transaction = getAndRemoveTransaction(request);
549: if (transaction == null) {
550: response.sendError(HttpServletResponse.SC_NOT_FOUND);
551: return;
552: }
553:
554: // commit transaction
555: this .logger.debug("Committing transaction: "
556: + transaction.getUri());
557: try {
558: transaction.commit();
559: } catch (SQLException e) {
560: throw new ServletException("Error committing transaction: "
561: + transaction.getUri(), e);
562: }
563:
564: // output xml
565: try {
566: serializeXML(transaction, response);
567: } catch (Exception e) {
568: throw new ServletException(
569: "Error serializing transaction to XML.", e);
570: }
571:
572: }
573:
574: /**
575: * Rollback a transaction.
576: *
577: * @see javax.servlet.http.HttpServlet#doDelete(javax.servlet.http.HttpServletRequest,
578: * javax.servlet.http.HttpServletResponse)
579: */
580: protected void doDelete(HttpServletRequest request,
581: HttpServletResponse response) throws ServletException,
582: IOException {
583:
584: // get transaction
585: Transaction transaction = getAndRemoveTransaction(request);
586: if (transaction == null) {
587: response.sendError(HttpServletResponse.SC_NOT_FOUND);
588: return;
589: }
590:
591: // rollback transaction
592: this .logger.debug("Rolling back transaction: "
593: + transaction.getUri());
594: try {
595: transaction.rollback();
596: } catch (SQLException e) {
597: throw new ServletException(
598: "Error rolling back transaction: "
599: + transaction.getUri(), e);
600: }
601:
602: }
603:
604: /**
605: * @see javax.servlet.Servlet#destroy()
606: */
607: public void destroy() {
608:
609: // check for multiple calls
610: if (this .destroyed) {
611: log("Servlet already destroyed. This should not happen.");
612: return;
613: }
614:
615: // mark this servlet as destroyed so the harvester thread knows it
616: // has to exit.
617: this .destroyed = true;
618:
619: // notify the harvester thread
620: synchronized (this .semaphore) {
621: this .semaphore.notifyAll();
622: }
623: }
624:
625: /**
626: * This SAX XMLReader generates an XML document from a Transaction.
627: */
628: final class TransactionXMLReader extends ObjectXMLReader {
629:
630: private static final String PATTERN_TIMESTAMP = "yyyy-MM-dd'T'HH:mm:ss";
631:
632: private final static String ROOT_ELEMENT = "transaction";
633:
634: private final static String ELEMENT_URI = "uri";
635:
636: private final static String ELEMENT_CREATIONTIME = "creationTime";
637:
638: private final static String ELEMENT_LASTACCESSED = "lastAccessedTime";
639:
640: private final static String ELEMENT_MAXINACTIVEINTERVAL = "maxInactiveInterval";
641:
642: /**
643: * @see org.gomba.utils.xml.ObjectXMLReader#parse(org.gomba.utils.xml.ObjectInputSource)
644: */
645: public void parse(ObjectInputSource input) throws IOException,
646: SAXException {
647:
648: // Note that SimpleDateFormat objects cannot be used
649: // concurrently by multiple threads
650: SimpleDateFormat timestampFormatter = new SimpleDateFormat(
651: PATTERN_TIMESTAMP);
652:
653: Transaction transaction = (Transaction) input.getObject();
654:
655: this .handler.startDocument();
656: this .handler.startElement(ContentHandlerUtils.DUMMY_NSU,
657: ROOT_ELEMENT, ROOT_ELEMENT,
658: ContentHandlerUtils.DUMMY_ATTS);
659:
660: ContentHandlerUtils.tag(this .handler, ELEMENT_URI,
661: transaction.getUri());
662:
663: ContentHandlerUtils.tag(this .handler, ELEMENT_CREATIONTIME,
664: timestampFormatter.format(transaction
665: .getCreationTime()));
666:
667: ContentHandlerUtils.tag(this .handler, ELEMENT_LASTACCESSED,
668: timestampFormatter.format(new Date(transaction
669: .getLastAccessedTime())));
670:
671: ContentHandlerUtils.tag(this .handler,
672: ELEMENT_MAXINACTIVEINTERVAL, Integer
673: .toString(transaction
674: .getMaxInactiveInterval()));
675:
676: this .handler.endElement(ContentHandlerUtils.DUMMY_NSU,
677: ROOT_ELEMENT, ROOT_ELEMENT);
678: this .handler.endDocument();
679:
680: }
681:
682: }
683:
684: private class TransactionHarvester extends Thread {
685:
686: /**
687: * Private constructor
688: */
689: private TransactionHarvester() {
690: // Give this thread a useful name
691: super ("TransactionHarvester");
692: }
693:
694: public void run() {
695:
696: log(this + " started.");
697:
698: do {
699:
700: try {
701:
702: // create a temporary list of transactions
703: // to avoid synchronization problems
704: final List transactionsList = new ArrayList(
705: transactions.keySet());
706:
707: // scan for expired transactions
708: for (Iterator i = transactionsList.iterator(); i
709: .hasNext();) {
710: final String transactionUri = (String) i.next();
711: final Transaction transaction = (Transaction) transactions
712: .get(transactionUri);
713: if (transaction == null) {
714: // transaction has been committed or rolled back in the meantime...
715: continue;
716: }
717: if (transaction.isExpired()) {
718: log("Found expired transaction: "
719: + transaction.getUri());
720: try {
721: removeTransaction(transaction);
722: } catch (Exception e) {
723: log(
724: "Error removing expired transaction: "
725: + transaction.getUri(),
726: e);
727: } finally {
728: try {
729: transaction.rollback();
730: } catch (Exception e) {
731: log(
732: "Error rolling back expired transaction: "
733: + transaction
734: .getUri(),
735: e);
736: }
737: }
738: }
739: }
740:
741: // sleep interval or until notified
742: synchronized (TransactionServlet.this .semaphore) {
743: try {
744: TransactionServlet.this .semaphore
745: .wait(transactionTimeout * 1000);
746: } catch (InterruptedException e) {
747: log("Thread interrupted: "
748: + Thread.currentThread(), e);
749: }
750: }
751:
752: } catch (Throwable t) {
753: // this prevents the thread from dying for an uncaught
754: // exception
755: log("Error checking for expired transactions.", t);
756: }
757:
758: } while (!TransactionServlet.this .destroyed);
759:
760: log(this + " stopped.");
761: }
762:
763: }
764:
765: }
|