/*
This file is part of Cyclos (www.cyclos.org).
A project of the Social Trade Organisation (www.socialtrade.org).
Cyclos is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
Cyclos 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with Cyclos; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package nl.strohalm.cyclos.http;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import nl.strohalm.cyclos.annotations.Inject;
import nl.strohalm.cyclos.exceptions.ApplicationException;
import nl.strohalm.cyclos.utils.TransactionHelper;
import nl.strohalm.cyclos.utils.access.LoggedUser;
import nl.strohalm.cyclos.utils.transaction.CurrentTransactionData;
import nl.strohalm.cyclos.utils.transaction.TransactionEndListener;
import nl.strohalm.cyclos.webservices.WebServiceContext;
import org.apache.commons.lang.exception.NestableRuntimeException;
import org.apache.commons.logging.Log;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
/**
* Base filter for web services transaction management
* @author luis
*/
public abstract class BaseWebServiceTransactionFilter extends OncePerRequestFilter {
protected TransactionHelper transactionHelper;
protected Log log = getLog();
@Inject
public void setTransactionHelper(final TransactionHelper transactionHelper) {
this.transactionHelper = transactionHelper;
}
/**
* Indicates whether the response state should be applied on transaction rollback as well
*/
protected abstract boolean applyResponseStateOnRollback();
@Override
protected void execute(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) throws IOException, ServletException {
// Run the request in a new transaction
try {
transactionHelper.runInCurrentThread(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(final TransactionStatus status) {
// When debug is enabled, log the transaction begin and add a listener for transaction end log
prepareDebugLog();
// Execute the workflow
try {
onBeforeRunInTransaction(request, response);
doRunInTransaction(request, response, chain, status);
} catch (final Throwable t) {
try {
onError(request, response, t);
} catch (final IOException e) {
throw new NestableRuntimeException(e);
}
} finally {
try {
onTransactionEnd(request, response);
} catch (final IOException e) {
throw new NestableRuntimeException(e);
}
}
}
});
} catch (final NestableRuntimeException e) {
final Throwable cause = e.getCause();
if (cause instanceof IOException) {
throw (IOException) cause;
}
if (cause instanceof ServletException) {
throw (ServletException) cause;
}
throw e;
} finally {
// Cleanup the thread locals
WebServiceContext.cleanup();
LoggedUser.cleanup();
}
}
/**
* Returns the logger
*/
protected abstract Log getLog();
/**
* Returns the service name, which is used on logs
*/
protected abstract String getServiceName();
/**
* Should be implemented in order to determine whether silenced errors from services will be rethrown
*/
protected abstract boolean handleSilencedErrors();
/**
* Callback invoked before starting the transaction
*/
protected void onBeforeRunInTransaction(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
}
/**
* Callback invoked when the processing results in error
*/
protected void onError(final HttpServletRequest request, final HttpServletResponse response, final Throwable t) throws IOException {
}
/**
* Callback invoked when the transaction has ended
*/
protected void onTransactionEnd(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
}
/**
* Runs the filter chain inside a transaction
*/
private void doRunInTransaction(final HttpServletRequest request, final HttpServletResponse servletResponse, final FilterChain chain, final TransactionStatus status) throws Throwable {
try {
// As both web services implementations (CXF for SOAP and Jackson for REST) manually flushes the response before we have the time to end
// the transaction, we must use a custom response wrapper, which never flushes the buffer until we really want it
final ResettableHttpServletResponse response = new ResettableHttpServletResponse(servletResponse);
// Apply the response state after end
CurrentTransactionData.addTransactionEndListener(new TransactionEndListener() {
@Override
protected void onTransactionEnd(final boolean commit) {
if (commit || applyResponseStateOnRollback()) {
response.applyState();
}
}
});
// Process the filter chain
chain.doFilter(request, response);
// Handle silenced errors
final Throwable error = CurrentTransactionData.getError();
final Integer sc = response.getStatus();
if (error != null && (sc == null || sc == HttpServletResponse.SC_OK)) {
throw error;
}
} catch (final ApplicationException e) {
if (e.isShouldRollback()) {
status.setRollbackOnly();
}
if (handleSilencedErrors()) {
throw e;
}
} catch (final Throwable t) {
status.setRollbackOnly();
if (handleSilencedErrors()) {
throw t;
}
}
}
/**
* Both logs the transaction begin and adds a transaction end listener to log commits / rollbacks. No-op if log debug is not enabled.
*/
private void prepareDebugLog() {
if (log.isDebugEnabled()) {
log.debug("Running " + getServiceName() + " in a new transaction");
CurrentTransactionData.addTransactionEndListener(new TransactionEndListener() {
@Override
protected void onTransactionEnd(final boolean commit) {
if (commit) {
log.debug("Committed " + getServiceName() + " transaction");
} else {
log.debug("Rolled back " + getServiceName() + " transaction");
}
}
});
}
}
}