nl.strohalm.cyclos.struts.CyclosRequestProcessor.java Source code

Java tutorial

Introduction

Here is the source code for nl.strohalm.cyclos.struts.CyclosRequestProcessor.java

Source

/*
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.struts;

import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.Set;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import nl.strohalm.cyclos.annotations.Inject;
import nl.strohalm.cyclos.entities.exceptions.LockingException;
import nl.strohalm.cyclos.entities.settings.events.LocalSettingsChangeListener;
import nl.strohalm.cyclos.exceptions.ApplicationException;
import nl.strohalm.cyclos.http.ResettableHttpServletRequest;
import nl.strohalm.cyclos.http.ResettableHttpServletResponse;
import nl.strohalm.cyclos.services.access.exceptions.SystemOfflineException;
import nl.strohalm.cyclos.services.settings.SettingsService;
import nl.strohalm.cyclos.utils.ActionHelper;
import nl.strohalm.cyclos.utils.DataIteratorHelper;
import nl.strohalm.cyclos.utils.ExceptionHelper;
import nl.strohalm.cyclos.utils.RequestHelper;
import nl.strohalm.cyclos.utils.SpringHelper;
import nl.strohalm.cyclos.utils.access.LoggedUser;
import nl.strohalm.cyclos.utils.logging.LoggingHandler;
import nl.strohalm.cyclos.utils.logging.TraceLogDTO;
import nl.strohalm.cyclos.utils.transaction.CurrentTransactionData;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.action.ActionServlet;
import org.apache.struts.action.SecureTilesRequestProcessor;
import org.apache.struts.config.ForwardConfig;
import org.apache.struts.config.ModuleConfig;
import org.hibernate.FlushMode;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.connection.ConnectionProvider;
import org.hibernate.engine.SessionFactoryImplementor;
import org.springframework.orm.hibernate3.SessionHolder;
import org.springframework.transaction.support.TransactionSynchronizationManager;

/**
 * Custom struts request processor. Among other things, we control the DB transactions here, opening a read-write transaction for action execution
 * @author luis
 */
public class CyclosRequestProcessor extends SecureTilesRequestProcessor {

    public static class ExecutionResult {
        private boolean commit;
        private boolean errorWasSilenced;
        private boolean hasWrite;
        private boolean traceLog;
        private boolean longTransaction;
        private Throwable error;
        private ActionForward forward;

        public Throwable getError() {
            return error;
        }

        public ActionForward getForward() {
            return forward;
        }

        public boolean isCommit() {
            return commit;
        }

        public boolean isErrorWasSilenced() {
            return errorWasSilenced;
        }

        public boolean isHasWrite() {
            return hasWrite;
        }

        public boolean isLongTransaction() {
            return longTransaction;
        }

        public boolean isTraceLog() {
            return traceLog;
        }
    }

    public static final String EXECUTION_RESULT_KEY = "cyclos.executionResult";
    public static final String NO_TRANSACTION_KEY = "cyclos.noTransactionManagement";

    private static final Log LOG = LogFactory.getLog(CyclosRequestProcessor.class);

    private SettingsService settingsService;
    private LoggingHandler loggingHandler;
    private SessionFactoryImplementor sessionFactory;
    private ConnectionProvider connectionProvider;
    private ActionHelper actionHelper;

    public SettingsService getSettingsService() {
        return settingsService;
    }

    @Override
    public void init(final ActionServlet servlet, final ModuleConfig moduleConfig) throws ServletException {
        super.init(servlet, moduleConfig);
        SpringHelper.injectBeans(servlet.getServletContext(), this);

        final CyclosControllerConfig config = (CyclosControllerConfig) moduleConfig.getControllerConfig();
        settingsService.addListener(config);
        config.initialize(settingsService.getLocalSettings());
    }

    @Override
    public void process(final HttpServletRequest request, final HttpServletResponse response)
            throws IOException, ServletException {
        try {
            request.setAttribute(EXECUTION_RESULT_KEY, new ExecutionResult());
            super.process(request, response);
        } catch (final Exception e) {
            if (e instanceof IOException) {
                throw (IOException) e;
            } else if (e instanceof ServletException) {
                throw (ServletException) e;
            } else {
                throw new RuntimeException(e);
            }
        } finally {
            cleanUpTransaction(request);
        }
    }

    @Inject
    public void setActionHelper(final ActionHelper actionHelper) {
        this.actionHelper = actionHelper;
    }

    @Inject
    public void setLoggingHandler(final LoggingHandler loggingHandler) {
        this.loggingHandler = loggingHandler;
    }

    @Inject
    public void setSessionFactory(final SessionFactoryImplementor sessionFactory) {
        this.sessionFactory = sessionFactory;
        connectionProvider = sessionFactory.getConnectionProvider();
    }

    @Inject
    public void setSettingsService(final SettingsService settingsService) {
        this.settingsService = settingsService;
    }

    /**
     * Override action creation to inject spring beans
     */
    @Override
    protected Action processActionCreate(final HttpServletRequest request, final HttpServletResponse response,
            final ActionMapping actionMapping) throws IOException {
        synchronized (actions) {
            Action action = (Action) actions.get(actionMapping.getType());
            if (action != null) {
                return action;
            } else {
                action = super.processActionCreate(request, response, actionMapping);

                if (action == null) {
                    return null;
                }

                // Register the action as listener
                if (action instanceof LocalSettingsChangeListener) {
                    settingsService.addListener((LocalSettingsChangeListener) action);
                }

                // Inject the required beans
                try {
                    SpringHelper.injectBeans(getServletContext(), action);
                } catch (final Exception e) {
                    // we must remove the already added action instance (by super.processActionCreate(...))
                    actions.remove(actionMapping.getType());
                    LOG.error("Error injecting beans on " + action, e);
                    throw new IllegalStateException(e);
                }

                return action;
            }
        }
    }

    /**
     * Override form creation to remove the form from session if the request was triggered by the menu
     */
    @Override
    @SuppressWarnings("unchecked")
    protected ActionForm processActionForm(final HttpServletRequest request, final HttpServletResponse response,
            final ActionMapping actionMapping) {
        final HttpSession session = request.getSession();
        if (StringUtils.isEmpty(actionMapping.getName())) {
            return null;
        }
        if (RequestHelper.isFromMenu(request)) {
            session.removeAttribute(actionMapping.getName());
        }
        // Add form to session
        final ActionForm form = super.processActionForm(request, response, actionMapping);
        if ("session".equals(actionMapping.getScope())) {
            Set<String> sessionForms = (Set<String>) session.getAttribute("sessionForms");
            if (sessionForms == null) {
                sessionForms = new HashSet<String>();
                session.setAttribute("sessionForms", sessionForms);
            }
            sessionForms.add(actionMapping.getName());
        }
        return form;
    }

    /**
     * Here is where the actual action will be invoked. Before it, we open a read-write DB transaction.
     */
    @Override
    protected ActionForward processActionPerform(final HttpServletRequest request,
            final HttpServletResponse response, final Action action, final ActionForm form,
            final ActionMapping mapping) throws IOException, ServletException {
        // Clean previous session stored forms
        if (RequestHelper.isFromMenu(request)) {
            cleanSessionForms(request, form);
        }

        // The main processing happens inside a loop, because we need to retry the main execution after a locking exception / deadlock
        final ResettableHttpServletRequest resetableRequest = new ResettableHttpServletRequest(request);
        final ResettableHttpServletResponse resetableResponse = new ResettableHttpServletResponse(response);
        while (true) {
            try {
                final ExecutionResult result = executeAction(resetableRequest, resetableResponse, action, form,
                        mapping);
                // Apply the state
                resetableRequest.applyState();
                resetableResponse.applyState();
                return result.forward;
            } catch (final LockingException e) {
                // Retry the transaction, resetting the state
                resetableRequest.resetState();
                resetableResponse.resetState();
                logDebug(request, "Locking error - re-executing action");
            } catch (final SystemOfflineException e) {
                return ActionHelper.sendError(mapping, request, response, "error.systemOffline");
            }
        }
    }

    /**
     * If the given {@link ForwardConfig} will actually include something (probably a JSP), open a new read-only transaction, so lazy-loading and data
     * iteration will work
     */
    @Override
    protected void processForwardConfig(final HttpServletRequest request, final HttpServletResponse response,
            final ForwardConfig forward) throws IOException, ServletException {
        final ExecutionResult result = (ExecutionResult) request.getAttribute(EXECUTION_RESULT_KEY);
        final boolean isInclude = forward != null && !forward.getRedirect();
        final boolean needsReadOnlyConnection = isInclude && !result.longTransaction;

        if (needsReadOnlyConnection) {
            // When needed, open a new read-only connection for the include
            openReadOnlyConnection(request);
        }

        // The top-most invocation will manage transaction. Any includes won't
        final boolean managesTransaction = !noTransaction(request);
        if (managesTransaction) {
            request.setAttribute(NO_TRANSACTION_KEY, true);
        }

        try {
            super.processForwardConfig(request, response, forward);
        } catch (final IllegalStateException e) {
            LOG.warn("Error processing the forward to " + forward.getPath());
        } finally {
            if (managesTransaction) {
                // Remove the attribute, otherwise, the top-most invocations of commitOrRollbackTransaction() or rollbackTransaction() will do nothing
                request.removeAttribute(NO_TRANSACTION_KEY);
            }
            if (result.longTransaction) {
                // Close the long running read-write connection
                result.commit = false;
                commitOrRollbackTransaction(request);
            } else if (needsReadOnlyConnection) {
                rollbackReadOnlyConnection(request);
            }
        }
    }

    @Override
    protected HttpServletRequest processMultipart(final HttpServletRequest request) {
        final HttpServletRequest multipartRequest = super.processMultipart(request);
        if (multipartRequest != request) {
            request.setAttribute("multipartRequest", multipartRequest);
        }
        return multipartRequest;
    }

    @Override
    protected void processPopulate(final HttpServletRequest request, final HttpServletResponse response,
            final ActionForm form, final ActionMapping mapping) throws ServletException {
        try {
            super.processPopulate(request, response, form, mapping);
        } catch (final Exception e) {
            LOG.error("Error populating " + form + " in " + mapping.getPath(), e);
            request.getSession().setAttribute("errorKey", "error.validation");
            final RequestDispatcher rd = request.getRequestDispatcher("/do/error");
            try {
                rd.forward(request, response);
            } catch (final IOException e1) {
                LOG.error("Error while trying to forward to error page", e1);
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void cleanSessionForms(final HttpServletRequest request, final ActionForm form) {
        final HttpSession session = request.getSession();
        final Set<String> sessionForms = (Set<String>) session.getAttribute("sessionForms");
        if (sessionForms != null) {
            for (final String name : sessionForms) {
                final ActionForm currentForm = (ActionForm) session.getAttribute(name);
                if (currentForm != form) {
                    session.removeAttribute(name);
                }
            }
        }
    }

    private void cleanUpTransaction(final HttpServletRequest request) {
        if (noTransaction(request)) {
            return;
        }
        logDebug(request, "Cleaning up transaction");

        // Close any open iterators
        DataIteratorHelper.closeOpenIterators();

        // Close the session
        final SessionHolder holder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
        if (holder != null) {
            try {
                final Session session = holder.getSession();
                if (session.isOpen()) {
                    session.close();
                }
            } catch (final Exception e) {
                LOG.error("Error closing Hibernate session", e);
            }
            TransactionSynchronizationManager.unbindResourceIfPossible(sessionFactory);
        }

        // Close the connection
        final Connection connection = (Connection) TransactionSynchronizationManager
                .getResource(connectionProvider);
        if (connection != null) {
            try {
                connectionProvider.closeConnection(connection);
            } catch (final Exception e) {
                LOG.error("Error closing database connection", e);
            }
            TransactionSynchronizationManager.unbindResourceIfPossible(connectionProvider);
        }

        // Cleanup the Spring transaction data
        TransactionSynchronizationManager.setCurrentTransactionReadOnly(false);
        TransactionSynchronizationManager.setActualTransactionActive(false);

        // Cleanup the current transaction data
        CurrentTransactionData.cleanup();

        request.removeAttribute(EXECUTION_RESULT_KEY);
    }

    private void commitOrRollbackTransaction(final HttpServletRequest request)
            throws IOException, ServletException {
        if (noTransaction(request)) {
            return;
        }
        final ExecutionResult result = (ExecutionResult) request.getAttribute(EXECUTION_RESULT_KEY);
        final SessionHolder sessionHolder = getSessionHolder();
        // Commit or rollback the transaction
        boolean runCommitListeners = false;
        boolean lockingException = false;
        if (result.commit) {
            logDebug(request, "Committing transaction");
            runCommitListeners = true; // Marked as commit - should run commit listeners
            try {
                sessionHolder.getTransaction().commit();
            } catch (final Throwable t) {
                // In case of locking exceptions, we must make sure the correct exception type is returned, so the transaction will be retried
                lockingException = ExceptionHelper.isLockingException(t);
                result.error = t;
            }
        } else {
            if (result.error == null && !result.hasWrite) {
                // Transaction was semantically a commit, so, the commit listeners should run.
                // However, as there where no writes the transaction will be rolled back
                runCommitListeners = true;
                logDebug(request, "Nothing written to database. Rolling-back transaction");
            } else {
                logDebug(request, "Rolling-back transaction");
            }
            sessionHolder.getTransaction().rollback();
        }

        // Disconnect the session
        sessionHolder.getSession().disconnect();

        if (lockingException) {
            // There was a locking exception - throw it now, so the transaction will be retried
            cleanUpTransaction(request);
            throw new LockingException();
        }

        // Unbind the session holder, so that listeners which should open a new transaction on this same thread won't be messed up
        TransactionSynchronizationManager.unbindResourceIfPossible(sessionFactory);

        // Run the transaction listener
        CurrentTransactionData.detachListeners().runListeners(runCommitListeners);

        // Bind the session holder again
        TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder);

        // Log the execution if a regular user is logged in and this is not an AJAX request
        if (result.traceLog) {
            traceLog(request, result.error, result.commit);
        }

        // The resulting error was not silenced (i.e, by the BaseAction's try / catch. Log and rethrow
        if (result.error != null && !result.errorWasSilenced) {
            actionHelper.generateLog(request, servlet.getServletContext(), result.error);
            ActionHelper.throwException(result.error);
        }
    }

    private ExecutionResult doExecuteAction(final HttpServletRequest request, final HttpServletResponse response,
            final Action action, final ActionForm form, final ActionMapping mapping) {
        final ExecutionResult result = (ExecutionResult) request.getAttribute(EXECUTION_RESULT_KEY);
        try {
            result.forward = super.processActionPerform(request, response, action, form, mapping);
            // Get data from CurrentTransactionData
            result.error = CurrentTransactionData.getError();
            result.errorWasSilenced = result.error != null;
            result.longTransaction = DataIteratorHelper.hasOpenIteratorsRequiringOpenConnection();
            if (result.error == null) {
                final SessionHolder holder = getSessionHolder();
                holder.getSession().flush();
            }
            result.hasWrite = CurrentTransactionData.hasWrite();
            if (result.error instanceof ApplicationException) {
                // When there's an ApplicationError, we can still commit, depending on the flag
                result.commit = result.hasWrite && !((ApplicationException) result.error).isShouldRollback();
            } else {
                // The general case: commit if there are no errors
                result.commit = result.hasWrite && !result.errorWasSilenced;
            }
        } catch (final ApplicationException e) {
            result.commit = e.isShouldRollback() ? false : true;
            result.error = e;
        } catch (final Throwable e) {
            result.commit = false;
            result.error = e;
        }
        result.traceLog = generateTraceLog(request);
        return result;
    }

    private ExecutionResult executeAction(final HttpServletRequest request, final HttpServletResponse response,
            final Action action, final ActionForm form, final ActionMapping mapping)
            throws IOException, ServletException {
        // Open a new read-write connection
        try {
            openReadWriteConnection(request);
        } catch (final Exception e) {
            throw new SystemOfflineException();
        }

        // Execute the actual action
        final ExecutionResult result = doExecuteAction(request, response, action, form, mapping);

        // Commit or rollback transaction if the current request does not need a long transaction
        if (!result.longTransaction) {
            commitOrRollbackTransaction(request);
        } else {
            logDebug(request, "Keeping connection open because there are open iterators");
        }
        return result;
    }

    private boolean generateTraceLog(final HttpServletRequest request) {
        final String uri = request.getRequestURI();
        return LoggedUser.getAccessType() == LoggedUser.AccessType.USER && !RequestHelper.isAjax(request)
                && !uri.endsWith("/login") && !uri.endsWith("/logout");
    }

    private SessionHolder getSessionHolder() {
        return (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
    }

    private void logDebug(final HttpServletRequest request, final String message) {
        if (LOG.isDebugEnabled()) {
            final String method = RequestHelper.isValidation(request) ? "VALIDATION" : request.getMethod();
            LOG.debug(String.format("%s (%s): %s", request.getRequestURI(), method, message));
        }
    }

    private boolean noTransaction(final HttpServletRequest request) {
        return Boolean.TRUE.equals(request.getAttribute(NO_TRANSACTION_KEY));
    }

    private void openReadOnlyConnection(final HttpServletRequest request) {
        if (noTransaction(request)) {
            return;
        }
        logDebug(request, "Opening read-only transaction for include");

        final Connection connection = (Connection) TransactionSynchronizationManager
                .getResource(connectionProvider);

        final SessionHolder holder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
        final Session session = holder.getSession();
        session.setFlushMode(FlushMode.MANUAL);
        session.setDefaultReadOnly(true);
        session.reconnect(connection);

        TransactionSynchronizationManager.setCurrentTransactionReadOnly(true);
    }

    private void openReadWriteConnection(final HttpServletRequest request) throws IOException, ServletException {
        if (noTransaction(request)) {
            return;
        }
        logDebug(request, "Opening a new read-write transaction");
        // Open a read-write transaction
        Connection connection = null;
        Session session = null;
        SessionHolder holder = null;
        Transaction transaction = null;
        try {
            connection = connectionProvider.getConnection();
            TransactionSynchronizationManager.bindResource(connectionProvider, connection);
            session = sessionFactory.openSession(connection);
            holder = new SessionHolder(session);
            transaction = session.beginTransaction();
            holder.setTransaction(transaction);
            TransactionSynchronizationManager.bindResource(sessionFactory, holder);
            holder.setSynchronizedWithTransaction(true);
            TransactionSynchronizationManager.setActualTransactionActive(true);
            TransactionSynchronizationManager.setCurrentTransactionReadOnly(false);
        } catch (final Exception e) {
            if (connection != null) {
                try {
                    connectionProvider.closeConnection(connection);
                } catch (final SQLException e1) {
                    LOG.warn("Error closing connection", e1);
                } finally {
                    TransactionSynchronizationManager.unbindResourceIfPossible(connectionProvider);
                    TransactionSynchronizationManager.unbindResourceIfPossible(sessionFactory);
                }
            }
            LOG.error("Couldn't open a transaction", e);
            ActionHelper.throwException(e);
        }
    }

    private void rollbackReadOnlyConnection(final HttpServletRequest request) {
        if (noTransaction(request)) {
            return;
        }
        final Connection connection = (Connection) TransactionSynchronizationManager
                .getResource(connectionProvider);
        try {
            logDebug(request, "Rolling back read-only transaction");
            connection.rollback();
        } catch (final SQLException e) {
            throw new IllegalStateException(e);
        }
    }

    private void traceLog(final HttpServletRequest request, final Throwable error, final boolean hasWrite) {
        final HttpServletRequest multipartRequest = (HttpServletRequest) request.getAttribute("multipartRequest");
        final HttpServletRequest req = multipartRequest == null ? request : multipartRequest;
        final TraceLogDTO params = new TraceLogDTO();
        params.setUser(LoggedUser.user());
        params.setRemoteAddress(req.getRemoteAddr());
        params.setRequestMethod(req.getMethod());
        params.setPath(req.getRequestURI());
        params.setParameters(ActionHelper.getParameterMap(req));
        final HttpSession session = req.getSession(false);
        params.setSessionId(session == null ? null : session.getId());
        params.setError(error);
        params.setHasDatabaseWrites(hasWrite);
        loggingHandler.trace(params);
    }
}