Package org.gomba

Source Code of org.gomba.TransactionServlet$TransactionHarvester

package org.gomba;

import java.io.IOException;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;

import org.gomba.utils.servlet.ServletLogger;
import org.gomba.utils.token.IdGenerator;
import org.gomba.utils.xml.ContentHandlerUtils;
import org.gomba.utils.xml.ObjectInputSource;
import org.gomba.utils.xml.ObjectXMLReader;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
* This servlet implements transactions for RESTful web services. This servlet
* is used to start, commit or rollback a transaction. This servlet will be
* typically mapped to <code>/transactions/*</code>.
*
* HTTP methods are mapped to transaction operations in this way:
* <dl>
* <dt>POST</dt>
* <dd>Start a new transaction. Posting an empty resource will create a new
* transaction. In the future we may add an XML document type to specify
* transaction options.</dd>
* <dt>GET</dt>
* <dd>Get information about a transaction.</dd>
* <dt>PUT</dt>
* <dd>Commit a transaction. Putting an empty resource is allowed. In the
* future we may support PUTting the (updated) XML data returned by GET.</dd>
* <dt>DELETE</dt>
* <dd>Rollback a transaction and destroy it.</dd>
* </dl>
*
* The XML returned by this servlet in response to GET request looks like this:
*
* <pre>
*    &lt;transaction&gt;
*     &lt;uri&gt;http://domain.com/transactions/238476245jhg34589345k&lt;/uri&gt;
*     &lt;creationTime&gt;2004-12-25T00:00:00&lt;/creationTime&gt;
*     &lt;lastAccessedTime&gt;2004-12-25T00:10:00&lt;/lastAccessedTime&gt;
*     &lt;maxInactiveInterval&gt;600&lt;/maxInactiveInterval&gt;
*    &lt;/transaction&gt;
* </pre>
*
* <p>
* TODO: provide a DTD.
* </p>
*
* Init-params:
* <dl>
* <dt>transaction-id</dt>
* <dd>An expression that evaluates to the transaction id. May contain ${}
* parameters. This will typically be: ${path.0} (Required)</dd>
* <dt>transaction-uri</dt>
* <dd>The transaction URI. This is not a full-blown expression, ${} cannot be
* used here. Only ${transaction.id} will be replaced with actual transaction
* id. A typical setting is: http://domain.org/transactions/${transaction.id}.
* In a sense, this the reverse of the transaction-id expression: transaction-id
* is used to extract an id from a URI, while transaction-URI is used to
* generate a URI from an id. (Required)</dd>
* <dt>transaction-timeout</dt>
* <dd>The transaction timeout interval for all transactions created by this
* servlet. The specified timeout must be expressed in a whole number of
* seconds. Since version 0.8 this value may be exceeded, it depends on when the
* HarvesterThread processes the transaction. The default value is 30.
* (Optional)</dd>
* </dt>
* <dt>jvm-route</dt>
* <dd>Identifier which must be used in load balancing scenarios to enable
* session affinity. The identifier, which must be unique across all servers
* which participate in the cluster, will be appended to the generated
* transaction identifier, therefore allowing the front end proxy to always
* forward requests that belong to a particular transaction to the same
* instance. Value can be an expression evaluated at servlet initialization
* time, so don't use request-related expressions. The "systemProperty"
* parameter domain is particurarly useful for this setting.</dd>
* </dl>
*
* @author Flavio Tordini
* @version $Id: TransactionServlet.java,v 1.8 2005/12/07 11:19:01 flaviotordini
*          Exp $
* @see http://www.seairth.com/web/resttp.html,
*      http://www.xml.com/lpt/a/2004/08/11/rest.html,
*      http://lists.xml.org/archives/xml-dev/200402/msg00267.html
*      http://groups.yahoo.com/group/rest-discuss/message/4141
*/
public class TransactionServlet extends HttpServlet {

    /**
     * A regular expression used to build the transaction URI.
     */
    private final static Pattern TRANSACTION_ID_PATTERN = Pattern
            .compile("\\$\\{transaction.id\\}");

    /**
     * Name of the context attribute that holds the transactions Map. Mapping is
     * transaction URI to Trasaction.
     */
    protected final static String CONTEXT_ATTRIBUTE_NAME_TRANSACTIONS = "org_gomba_transactions";

    /**
     * Name of the context attribute that holds the servlet instance.
     */
    protected final static String CONTEXT_ATTRIBUTE_NAME_SERVLET = "org_gomba_transactionServlet";

    private final static String INIT_PARAM_TRANSACTION_URI = "transaction-uri";

    private final static String INIT_PARAM_TRANSACTION_ID = "transaction-id";

    private final static String INIT_PARAM_TRANSACTION_TIMEOUT = "transaction-timeout";

    private final static String INIT_PARAM_JVMROUTE = "jvm-route";

    private final static int DEFAULT_TRANSACTION_TIMEOUT = 30;

    /**
     * <code>true</code> if debug logging is turned on.
     */
    private boolean debugMode;

    /**
     * A logger to be passed around to enable servlet logging in non-servlet
     * classes.
     */
    private ServletLogger logger;

    /**
     * The data source to query.
     */
    private DataSource dataSource;

    /**
     * Expression that evaluates to the transaction id. Will tipically be the
     * first extra path element. The id of a transaction is private affair of
     * this servlet. Clients refer to a transaction using its URI.
     */
    private Expression idExpression;

    /**
     * Pseudo-expression that evaluates to the transaction URI. This is not a
     * full-blown expression, only the <code>TRANSACTION_ID_PATTERN</code> is
     * replaced with actual transaction id.
     */
    private String uriExpression;

    /**
     * Transaction timeout in seconds
     */
    private int transactionTimeout = DEFAULT_TRANSACTION_TIMEOUT;

    /**
     * Map containing current active transactions. Mapping is transaction URI to
     * Transaction.
     */
    private Map transactions;

    /**
     * This will help us generate secure random transaction ids
     */
    private final IdGenerator idGenerator = new IdGenerator();

    /**
     * <code>true</code> when the destroy() method has been called by the
     * servlet container.
     */
    private boolean destroyed;

    /**
     * Dummy object acting as a semaphore for the harvester thread.
     */
    private final Object semaphore = new Object();

    /**
     * Server identifier
     */
    private String jvmRoute;
   
    /**
     * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
     */
    public void init(ServletConfig config) throws ServletException {
        super.init(config);

        // debug mode
        this.debugMode = AbstractServlet.getDebugMode(config);

        // build our strange logger
        this.logger = new ServletLogger(this, this.debugMode);

        // get the JNDI data source
        this.dataSource = AbstractServlet.getDataSource(config);

        // transaction URI pseudo-expression
        this.uriExpression = config
                .getInitParameter(INIT_PARAM_TRANSACTION_URI);
        if (this.uriExpression == null) {
            throw new ServletException("Missing init-param: "
                    + INIT_PARAM_TRANSACTION_URI);
        }

        // transaction id expression
        String idExpressionStr = config
                .getInitParameter(INIT_PARAM_TRANSACTION_ID);
        if (idExpressionStr == null) {
            throw new ServletException("Missing init-param: "
                    + INIT_PARAM_TRANSACTION_ID);
        }
        try {
            this.idExpression = new Expression(idExpressionStr);
        } catch (Exception e) {
            throw new ServletException("Error parsing "
                    + INIT_PARAM_TRANSACTION_ID + " expression.", e);
        }

        // transaction timeout
        String transactionTimeoutStr = config
                .getInitParameter(INIT_PARAM_TRANSACTION_TIMEOUT);
        if (transactionTimeoutStr != null) {
            this.transactionTimeout = Integer.parseInt(transactionTimeoutStr);
        }

        // server identifier
        String jvmRouteStr = config.getInitParameter(INIT_PARAM_JVMROUTE);
        if (jvmRouteStr != null) {
            Expression jvmRouteExpression;
            try {
                jvmRouteExpression = new Expression(jvmRouteStr);
            } catch (Exception e) {
                throw new ServletException("Error parsing "
                        + INIT_PARAM_JVMROUTE + " expression.", e);
            }
            // domains using the request will throw a npe
            ParameterResolver parameterResolver = new ParameterResolver(null);
            try {
                this.jvmRoute = jvmRouteExpression.replaceParameters(parameterResolver).toString();
            } catch (Exception e) {
                throw new ServletException("Error evaluating "
                        + INIT_PARAM_JVMROUTE + " expression.", e);
            }
        }

        // put this servlet instance in application scope
        // FIXME servlets shold not be put in scopes
        if (getServletContext().getAttribute(CONTEXT_ATTRIBUTE_NAME_SERVLET) != null) {
            throw new ServletException("A " + this.getClass().getName()
                    + " is already configured for the current context.");
        }
        getServletContext().setAttribute(CONTEXT_ATTRIBUTE_NAME_SERVLET, this);

        // init the transactions map
        // since we're in a servlet env where multiple thread will modify the
    // map, we need a synchronized impl
        this.transactions = Collections.synchronizedMap(new HashMap());
        getServletContext().setAttribute(CONTEXT_ATTRIBUTE_NAME_TRANSACTIONS,
                this.transactions);

        // start background thread that removes expired transactions
        TransactionHarvester harvester = new TransactionHarvester();
        harvester.setDaemon(true);
        harvester.setPriority(Thread.MIN_PRIORITY);
        harvester.start();

    }

    /**
     * Create a new transaction and add it the map.
     */
    protected  Transaction createTransaction()
            throws ServletException {

        final Transaction transaction;

        // build transaction URI
        // since this block is not synchronized
        // there is a slight chance to generate an existing id
        String transactionURI;
        do {
          // generate id
            String transactionId = this.idGenerator.generateId();
            if (this.jvmRoute != null) {
                transactionId += '.' + this.jvmRoute;
            }
            transactionURI = getTransactionURI(transactionId);
        } while (this.transactions.containsKey(transactionURI));

        // create transaction
        transaction = new Transaction(this.logger, this.dataSource,
                transactionURI, this.transactionTimeout);

        // add transaction to our map
        this.transactions.put(transaction.getUri(), transaction);

        this.logger.debug("Created transaction: " + transaction.getUri());

        return transaction;
    }

    /**
     * Create a transaction.
     *
     * @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest,
     *           javax.servlet.http.HttpServletResponse)
     */
    protected void doPost(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {

        // TODO do we need XML from the request body ???
        // TODO if we need it, it should be customizable with XSLT

        // create the transaction
        Transaction transaction = createTransaction();

        // 201!
        response.setStatus(HttpServletResponse.SC_CREATED);

        // set Location header
        response.setHeader("Location", transaction.getUri());

        // TODO output XML with XLink
        // TODO and make it customizable via XSLT

    }

    /**
     * Get the transaction id by evaluating the id expression.
     *
     * @return Never returns null
     */
    private String getTransactionId(ParameterResolver parameterResolver)
            throws ServletException {

        Object obj;
        try {
            obj = this.idExpression.replaceParameters(parameterResolver);
        } catch (Exception e) {
            throw new ServletException(
                    "Error evaluating transaction id expression.", e);
        }

        if (obj instanceof java.lang.String) {
            return (String) obj;
        }

        throw new ServletException(
                "Transaction id expression does not evaluate to String: "
                        + obj.getClass().getName());

    }

    /**
     * Get the transaction URI by evaluating the URI pseudo expression.
     */
    private String getTransactionURI(String transactionId) {
        Matcher m = TRANSACTION_ID_PATTERN.matcher(this.uriExpression);
        return m.replaceFirst(transactionId);
    }

    /**
     * Get the Transaction object for the specified request.
     *
     * @return May return null if the specified transaction does not exist.
     */
    private Transaction getTransaction(HttpServletRequest request)
            throws ServletException {

        // create the parameter resolver that will help us throughout this
        // request
        final ParameterResolver parameterResolver = new ParameterResolver(
                request);

        // get transaction id
        String transactionId = getTransactionId(parameterResolver);

        // get transaction URI
        String transactionURI = getTransactionURI(transactionId);

        // get transaction
        Transaction transaction = (Transaction) this.transactions
                .get(transactionURI);

        if (transaction == null) {
            log("Invalid or expired transaction: " + transactionURI);
        }

        return transaction;

    }

    /**
     * Get the Transaction object for the specified request.
     *
     * @return May return null if the specified transaction does not exist.
     */
    private Transaction getAndRemoveTransaction(HttpServletRequest request)
            throws ServletException {

        // create the parameter resolver that will help us throughout this
        // request
        final ParameterResolver parameterResolver = new ParameterResolver(
                request);

        // get transaction id
        String transactionId = getTransactionId(parameterResolver);

        // get transaction URI
        String transactionURI = getTransactionURI(transactionId);

        // get transaction
        Transaction transaction = (Transaction) this.transactions
                .remove(transactionURI);

        if (transaction == null) {
            log("Invalid, expired or completed transaction: " + transactionURI);
        }

        return transaction;

    }
   
    /**
     * Obtain information about a transaction.
     *
     * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest,
     *           javax.servlet.http.HttpServletResponse)
     */
    protected void doGet(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {

        // get transaction
        Transaction transaction = getTransaction(request);
        if (transaction == null) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // output xml
        try {
            serializeXML(transaction, response);
        } catch (Exception e) {
            throw new ServletException("Error serializing transaction to XML.",
                    e);
        }

    }

    /**
     * Remove a transaction from the map.
     */
    private void removeTransaction(Transaction transaction)
            throws ServletException {
      Object previousValue = this.transactions.remove(transaction.getUri());
        if (previousValue == null) {
            log("Transaction already removed: " + transaction.getUri());
        }
    }

    /**
     * Serialize an object to XML using SAX and TrAX APIs in a smart way.
     * (dagnele sucks :)
     *
     * @param object
     *                   The object to serialize
     * @param saxReader
     *                   The SAX "parser"
     * @param response
     *                   The HTTP response
     * @see <a
     *           href="http://java.sun.com/j2se/1.4.2/docs/api/javax/xml/transform/package-summary.html">TrAX
     *           API </a>
     */
    private void serializeXML(Transaction object, HttpServletResponse response)
            throws TransformerException, IOException {

        ObjectXMLReader saxReader = new TransactionXMLReader();

        // Let the HTTP client know the output content type
        response.setContentType("text/xml");

        // Create TrAX Transformer
        Transformer t = TransformerFactory.newInstance().newTransformer();

        // Set trasformation output properties
        t.setOutputProperty(OutputKeys.ENCODING, response
                .getCharacterEncoding());

        // Create the trasformation source using our custom ObjectInputSource
        InputSource inputSource = new ObjectInputSource(object);
        Source source = new SAXSource(saxReader, inputSource);

        // Create the trasformation result
        // Result result = new StreamResult(response.getWriter());
        Result result = new StreamResult(response.getOutputStream());

        // Go!
        t.transform(source, result);

    }

    /**
   * Commit a transaction.
   *
   * @see javax.servlet.http.HttpServlet#doPut(javax.servlet.http.HttpServletRequest,
   *      javax.servlet.http.HttpServletResponse)
   */
  protected void doPut(HttpServletRequest request,
      HttpServletResponse response) throws ServletException, IOException {

    // get transaction
    Transaction transaction = getAndRemoveTransaction(request);
    if (transaction == null) {
      response.sendError(HttpServletResponse.SC_NOT_FOUND);
      return;
    }

    // commit transaction
    this.logger.debug("Committing transaction: " + transaction.getUri());
    try {
      transaction.commit();
    } catch (SQLException e) {
      throw new ServletException("Error committing transaction: "
          + transaction.getUri(), e);
    }

    // output xml
    try {
      serializeXML(transaction, response);
    } catch (Exception e) {
      throw new ServletException("Error serializing transaction to XML.",
          e);
    }

  }

    /**
   * Rollback a transaction.
   *
   * @see javax.servlet.http.HttpServlet#doDelete(javax.servlet.http.HttpServletRequest,
   *      javax.servlet.http.HttpServletResponse)
   */
  protected void doDelete(HttpServletRequest request,
      HttpServletResponse response) throws ServletException, IOException {

    // get transaction
    Transaction transaction = getAndRemoveTransaction(request);
    if (transaction == null) {
      response.sendError(HttpServletResponse.SC_NOT_FOUND);
      return;
    }

    // rollback transaction
    this.logger.debug("Rolling back transaction: " + transaction.getUri());
    try {
      transaction.rollback();
    } catch (SQLException e) {
      throw new ServletException("Error rolling back transaction: "
          + transaction.getUri(), e);
    }

  }

    /**
   * @see javax.servlet.Servlet#destroy()
   */
    public void destroy() {

        // check for multiple calls
        if (this.destroyed) {
            log("Servlet already destroyed. This should not happen.");
            return;
        }

        // mark this servlet as destroyed so the harvester thread knows it
        // has to exit.
        this.destroyed = true;

        // notify the harvester thread
        synchronized (this.semaphore) {
            this.semaphore.notifyAll();
        }
    }

    /**
     * This SAX XMLReader generates an XML document from a Transaction.
     */
    final class TransactionXMLReader extends ObjectXMLReader {

        private static final String PATTERN_TIMESTAMP = "yyyy-MM-dd'T'HH:mm:ss";

        private final static String ROOT_ELEMENT = "transaction";

        private final static String ELEMENT_URI = "uri";

        private final static String ELEMENT_CREATIONTIME = "creationTime";

        private final static String ELEMENT_LASTACCESSED = "lastAccessedTime";

        private final static String ELEMENT_MAXINACTIVEINTERVAL = "maxInactiveInterval";

        /**
         * @see org.gomba.utils.xml.ObjectXMLReader#parse(org.gomba.utils.xml.ObjectInputSource)
         */
        public void parse(ObjectInputSource input) throws IOException,
                SAXException {

            // Note that SimpleDateFormat objects cannot be used
            // concurrently by multiple threads
            SimpleDateFormat timestampFormatter = new SimpleDateFormat(
                    PATTERN_TIMESTAMP);

            Transaction transaction = (Transaction) input.getObject();

            this.handler.startDocument();
            this.handler.startElement(ContentHandlerUtils.DUMMY_NSU,
                    ROOT_ELEMENT, ROOT_ELEMENT, ContentHandlerUtils.DUMMY_ATTS);

            ContentHandlerUtils.tag(this.handler, ELEMENT_URI, transaction
                    .getUri());

            ContentHandlerUtils.tag(this.handler, ELEMENT_CREATIONTIME,
                    timestampFormatter.format(transaction.getCreationTime()));

            ContentHandlerUtils.tag(this.handler, ELEMENT_LASTACCESSED,
                    timestampFormatter.format(new Date(transaction
                            .getLastAccessedTime())));

            ContentHandlerUtils.tag(this.handler, ELEMENT_MAXINACTIVEINTERVAL,
                    Integer.toString(transaction.getMaxInactiveInterval()));

            this.handler.endElement(ContentHandlerUtils.DUMMY_NSU,
                    ROOT_ELEMENT, ROOT_ELEMENT);
            this.handler.endDocument();

        }

    }

    private class TransactionHarvester extends Thread {

        /**
         * Private constructor
         */
        private TransactionHarvester() {
            // Give this thread a useful name
            super("TransactionHarvester");
        }

        public void run() {

      log(this + " started.");

      do {

        try {

          // create a temporary list of transactions
          // to avoid synchronization problems
          final List transactionsList = new ArrayList(transactions.keySet());

          // scan for expired transactions
          for (Iterator i = transactionsList.iterator(); i.hasNext();) {
            final String transactionUri = (String) i.next();
                        final Transaction transaction = (Transaction) transactions.get(transactionUri);
                        if (transaction == null) {
                            // transaction has been committed or rolled back in the meantime...
                            continue;
                        }
            if (transaction.isExpired()) {
              log("Found expired transaction: "
                  + transaction.getUri());
              try {
                removeTransaction(transaction);
              } catch (Exception e) {
                log("Error removing expired transaction: "
                    + transaction.getUri(), e);
              } finally {
                try {
                  transaction.rollback();
                } catch (Exception e) {
                  log(
                      "Error rolling back expired transaction: "
                          + transaction.getUri(), e);
                }
              }
            }
          }

          // sleep interval or until notified
          synchronized (TransactionServlet.this.semaphore) {
            try {
              TransactionServlet.this.semaphore
                  .wait(transactionTimeout * 1000);
            } catch (InterruptedException e) {
              log(
                  "Thread interrupted: "
                      + Thread.currentThread(), e);
            }
          }

        } catch (Throwable t) {
          // this prevents the thread from dying for an uncaught
          // exception
          log("Error checking for expired transactions.", t);
        }

      } while (!TransactionServlet.this.destroyed);

      log(this + " stopped.");
    }

    }

}
TOP

Related Classes of org.gomba.TransactionServlet$TransactionHarvester

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.