/*
* EuroCarbDB, a framework for carbohydrate bioinformatics
*
* Copyright (c) 2006-2009, Eurocarb project, or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
* A copy of this license accompanies this distribution in the file LICENSE.txt.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* Last commit: $Rev: 1549 $ by $Author: glycoslave $ on $Date:: 2009-07-19 #$
*/
package org.eurocarbdb.interceptor;
// stdlib imports
// 3rd party imports
import org.apache.log4j.Logger;
import com.opensymphony.xwork.Action;
import com.opensymphony.xwork.ActionSupport;
import com.opensymphony.xwork.Result;
import com.opensymphony.xwork.ActionContext;
import com.opensymphony.xwork.ActionInvocation;
import com.opensymphony.xwork.interceptor.Interceptor;
import com.opensymphony.xwork.interceptor.PreResultListener;
import com.opensymphony.xwork.interceptor.ExceptionMappingInterceptor;
import com.opensymphony.xwork.interceptor.ExceptionHolder;
import org.hibernate.HibernateException;
import org.hibernate.FlushMode;
// eurocarb imports
import org.eurocarbdb.dataaccess.EntityManager;
import org.eurocarbdb.dataaccess.HibernateEntityManager;
// static imports
import static org.eurocarbdb.dataaccess.Eurocarb.getEntityManager;
/**
*<p>
* Interceptor that manages the current persistence context.
* This interceptor performs 3 significant functions:
*<ul>
* <li>Catches any/all {@link Exception}s thrown by {@link Action}s
* and nested {@link Interceptor}s. See 'Transaction' and 'Exception'
* sections below.</li>
* <li>Executes a manual {@link EntityManager.flush()} of the
* persistence context <em>after</em> the Action is executed, but
* <em>before</em> the View is executed or rendered.</li>
* <li>Sets the persitence context as read-only for the rendering of the View.</li>
*</ul>
*</p>
*
*<h2>Lifecycle</h2>
*<p>
*<ol>
* <li>Request is received by client.</li>
* <li>{@link #intercept} is called.</li>
* <li>{@link #handlePreExecutePhase} is called.</li>
* <li>{@link Action} is executed (by <code>super.intercept</code>)</li>
* <li>If no Exceptions were thrown, {@link #handlePostExecutePreViewPhase}
* is called, which flushes the persistence context. If there was an
* Exception, see 'Exceptions' section below.</li>
* <li>{@link #handlePostExecutePostViewPhase} is called, which
* sets the persistence context to be <em>read-only</em></li>
* <li>Control is allowed to pass to the View, and view is rendered.</li>
* <li>Request to sent to client.</li>
*</ol>
*</p>
*
*<h2>Transactions</h2>
*<p>
* Transactions are not opened and closed by this class (see
* {@link org.eurocarbdb.servlet.filter.HibernateSessionRequestFilter}),
* unless an Exception is caught, in which case the current transaction
* is aborted by calling {@link EntityManager.abortUnitOfWork()}.
*<p>
*
*<h2>Exceptions</h2>
*<p>
* If an {@link Exception} is caught during Action execution (this includes
* Exceptions thrown by other {@link Interceptor}s), the webwork base class
* {@link ExceptionMappingInterceptor} compares the class of the Exception
* to the list of configured exception-mappings in the application config
* (conf/xwork.xml). If mapped, the Exception is consumed (and logged) and
* the mapped result name returned as the result code for the Action. See
* <a href="http://www.opensymphony.com/webwork/wikidocs/Exception%20Handling.html">
* the online docs</a> or {@link ExceptionMappingInterceptor} for further info.
*</p>
*<p>
* If an Exception class is <em>not</em> mapped, the Exception is allowed
* to propagate (and probably return a Exception trace to the browser/user/console).
* This is for ease of debugging. Obviously, for production we want all
* Exceptions to be mapped.
*</p>
*
* @see org.eurocarbdb.dataaccess.EntityManager
* @see org.eurocarbdb.dataaccess.HibernateEntityManager
* @see ActionInvocation
*
* @version $Rev: 1549 $
* @author mjh
*/
public class PersistenceLifecycleInterceptor extends ExceptionMappingInterceptor
implements Interceptor, PreResultListener
{
/** Logging handle. */
private static final Logger log = Logger.getLogger( PersistenceLifecycleInterceptor.class );
private EntityManager em;
private boolean exceptionWasThrown = false;
private boolean exceptionIsHandled = false;
private boolean unitOfWorkAborted = false;
private long start = System.currentTimeMillis();
private long elapsed = 0;
/** Called implicitly by webwork when class is first used */
public void init()
{
if ( log.isDebugEnabled() )
{
log.debug(
this.getClass().getName()
+ " loaded"
);
}
super.init();
}
/** Called implicitly by webwork on application shutdown */
public void destroy()
{
super.destroy();
}
/**
* Calls {@link HibernateEntityManager.flush()} for every
* {@link ActionInvocation} that completes without throwing
* an Exception.
*
* Also logs the result string returned by the underlying {@link Action}.
*/
public String intercept( ActionInvocation ai )
throws Exception
{
// add flush handler: "preResult" means that the 'beforeResult'
// method will be called *after* the Action gets executed, but
// *before* control has been passed to the View.
ai.addPreResultListener( this );
this.em = getEntityManager();
handlePreExecutePhase();
try
{
//*** action executes here ***
String result_code = super.intercept( ai );
// any code from here on executes *after* the view has been rendered
log.info("Action returned result code: " + result_code );
return result_code;
}
catch ( Exception ex )
{
this.exceptionWasThrown = true;
this.exceptionIsHandled = false;
handlePostExecuteException( ex );
throw ex;
}
finally
{
handlePostExecutePostViewPhase();
}
}
/**
* This method calls {@link #handlePostExecutePreViewPhase()}
* after the {@link Action} is run, but before the View phase
* has rendered the {@link Result}. It is hooked into
* the action lifecycle by {@link ActionInvocation.addPreResultListener}
* called during the call to {@link intercept}.
*/
public void beforeResult( ActionInvocation ai, String resultCode )
{
handlePostExecutePreViewPhase();
}
/**
* Manually flushes the Hibernate session, if there is one, thereby
* committing unsaved data to the database in the form of inserts/updates.
*/
public void flush()
{
EntityManager em = getEntityManager();
if ( em instanceof HibernateEntityManager )
{
log.debug("flushing Hibernate session");
HibernateEntityManager hem = (HibernateEntityManager) em;
// try
// {
hem.flush(); // allow flush exceptions to be thrown & caught
// }
// catch ( Exception ex )
// {
// log.warn("Caught exception while trying to flush data store", ex );
// this.exceptionWasThrown = true;
// throw ex;
// }
// finally
// {
// // change the FlushMode to effectively read-only to render
// view to improve Hibernate performance
log.debug("setting Hibernate flush_mode to MANUAL (ie: read-only)");
hem.getHibernateSession().setFlushMode( FlushMode.MANUAL );
// }
}
else log.debug("(not a hibernate entity manager)");
}
/**
* Called before Action or View have been executed. Current
* implementation sets the Hibernate Session to be
* {@link FlushMode.AUTO} (which it probably is already..).
*/
protected void handlePreExecutePhase()
{
// reset Hibernate FlushMode to AUTO (just in case it
// wasn't already). AUTO means DB operations will be performed
// asynchronously, batched together near the end of the transaction
// to improve performance & concurrency.
if ( em instanceof HibernateEntityManager )
{
HibernateEntityManager hem = (HibernateEntityManager) em;
log.trace("resetting Hibernate flush_mode to AUTO");
hem.getHibernateSession().setFlushMode( FlushMode.AUTO );
}
log.info("time elapsed since init: " + elapsed() + "millsecs");
}
/**
* Called *after* action has been executed but *before* View has
* been executed or rendered; current implementation calls
* {@link EntityManager.flush()}.
*/
protected void handlePostExecutePreViewPhase()
{
elapsed();
flush();
log.debug("flush took " + elapsed() + "millsecs");
}
/**
* Called *after* action and view have both been executed.
* Current implementation does nothing.
*/
protected void handlePostExecutePostViewPhase()
{
log.info("rendering of view took " + elapsed() + "millsecs");
/*
if ( hem != null && hem.getHibernateSession().getTransaction().isActive() )
{
log.debug("force clearing session");
hem.getHibernateSession().clear();
}
*/
}
/**
* Called if any {@link Exception} was thrown by the {@link Action}
* or nested {@link Interceptor}s. Current implementation does the
* following:
*<ol>
* <li>If the Exception is configured to be handleable by the application,
* then the method logs the exception and returns.</li>
* <li>If the Exception is not configured as handleable, then the
* Exception is allowed to propagate.
*</ol>
* In both cases, the current transaction is aborted.
*
* @see EntityManager.abortUnitOfWork()
*/
protected void handlePostExecuteException( Exception ex )
{
if ( unitOfWorkAborted )
return;
assert exceptionWasThrown == true;
if ( exceptionIsHandled )
{
log.warn(
"Caught handled exception transaction will be aborted "
+ "and an exception result will be rendered",
ex
);
}
else
{
log.fatal(
"Caught unhandled exception ("
+ ex.getClass().getSimpleName()
+ "), transaction will be aborted "
+ "and exception rethrown"
);
}
getEntityManager().abortUnitOfWork();
unitOfWorkAborted = true;
return;
}
@Override
protected void handleLogging( Exception ex )
{
this.exceptionWasThrown = true;
this.exceptionIsHandled = true;
handlePostExecuteException( ex );
super.handleLogging( ex );
}
/**
* This method is ONLY ever called if 1) an {@link Exception} was
* thrown by application code, and 2) the Exception thrown has been
* mapped in the app config to be mapped to a result code
* (the 'exception-mappings' element).
*
* Exceptions that are NOT mapped will not cause this method to be
* called but are simply handled as a regular exception.
*/
@Override
protected void publishException( ActionInvocation inv, ExceptionHolder exh )
{
this.exceptionWasThrown = true;
this.exceptionIsHandled = true;
handlePostExecuteException( exh.getException() );
if (inv.getAction() instanceof ActionSupport ) {
((ActionSupport) inv.getAction()).addActionError("An error occurred generating this page");
}
super.publishException( inv, exh );
}
private final int elapsed()
{
long now = System.currentTimeMillis();
elapsed = now - start;
start = now;
return (int) elapsed;
}
} // end class PersistenceLifecycleInterceptor