package com.jeeww.core.ext;

import java.io.IOException;
import java.util.concurrent.Callable;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.hibernate.FlushMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.orm.hibernate4.SessionFactoryUtils;
import org.springframework.orm.hibernate4.SessionHolder;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.CallableProcessingInterceptorAdapter;
import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.filter.OncePerRequestFilter;

/**
 * Servlet 2.3 Filter that binds a Hibernate Session to the thread for the entire processing of the request. Intended
 * for the "Open Session in View" pattern, i.e. to allow for lazy loading in web views despite the original transactions
 * already being completed.
 * <p>
 * This filter makes Hibernate Sessions available via the current thread, which will be autodetected by transaction
 * managers. It is suitable for service layer transactions via
 * {@link org.springframework.orm.hibernate4.HibernateTransactionManager} as well as for non-transactional execution (if
 * configured appropriately).
 * <p>
 * <b>NOTE</b>: This filter will by default <i>not</i> flush the Hibernate Session, with the flush mode set to
 * {@code FlushMode.NEVER}. It assumes to be used in combination with service layer transactions that care for the
 * flushing: The active transaction manager will temporarily change the flush mode to {@code FlushMode.AUTO} during a
 * read-write transaction, with the flush mode reset to {@code FlushMode.NEVER} at the end of each transaction.
 * <p>
 * <b>WARNING:</b> Applying this filter to existing logic can cause issues that have not appeared before, through the
 * use of a single Hibernate Session for the processing of an entire request. In particular, the reassociation of
 * persistent objects with a Hibernate Session has to occur at the very beginning of request processing, to avoid
 * clashes with already loaded instances of the same objects.
 * <p>
 * Looks up the SessionFactory in Spring's root web application context. Supports a "sessionFactoryBeanName" filter
 * init-param in {@code web.xml}; the default bean name is "sessionFactory". Looks up the SessionFactory on each
 * request, to avoid initialization order issues (when using ContextLoaderServlet, the root application context will get
 * initialized <i>after</i> this filter).
 * @author Juergen Hoeller
 * @since 3.1
 * @see #lookupSessionFactory
 * @see OpenSessionInViewInterceptor
 * @see org.springframework.orm.hibernate4.HibernateTransactionManager
 * @see org.springframework.transaction.support.TransactionSynchronizationManager
 */
public class OpenSessionInViewFilter extends OncePerRequestFilter {
    /**
     * .
     */
    public static final String DEFAULT_SESSION_FACTORY_BEAN_NAME = "sessionFactory";
    /**
     * .
     */
    private String sessionFactoryBeanName = DEFAULT_SESSION_FACTORY_BEAN_NAME;

    /**
     * Set the bean name of the SessionFactory to fetch from Spring's root application context. Default is
     * "sessionFactory".
     * @see #DEFAULT_SESSION_FACTORY_BEAN_NAME
     * @param sessionFactoryBeanNamea a.
     */
    public final void setSessionFactoryBeanName(final String sessionFactoryBeanNamea) {
        this.sessionFactoryBeanName = sessionFactoryBeanNamea;
    }

    /**
     * @return Return the bean name of the SessionFactory to fetch from Spring's root application context.
     */
    protected final String getSessionFactoryBeanName() {
        return this.sessionFactoryBeanName;
    }

    /**
     * @return Returns "false" so that the filter may re-bind the opened Hibernate {@code Session} to each
     *         asynchronously dispatched thread and postpone closing it until the very last asynchronous dispatch.
     */
    @Override
    protected final boolean shouldNotFilterAsyncDispatch() {
        return false;
    }

    /**
     * @return Returns "false" so that the filter may provide a Hibernate {@code Session} to each error dispatches.
     */
    @Override
    protected final boolean shouldNotFilterErrorDispatch() {
        return false;
    }

    @Override
    protected final void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response,
            final FilterChain filterChain) throws ServletException, IOException {
        SessionFactory sessionFactory = lookupSessionFactory(request);
        boolean participate = false;
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        String key = getAlreadyFilteredAttributeName();
        if (TransactionSynchronizationManager.hasResource(sessionFactory)) {
            participate = true;
        } else {
            boolean isFirstRequest = !isAsyncDispatch(request);
            if (isFirstRequest || !applySessionBindingInterceptor(asyncManager, key)) {
                logger.debug("Opening Hibernate Session in OpenSessionInViewFilter");
                Session session = openSession(sessionFactory);
                SessionHolder sessionHolder = new SessionHolder(session);
                TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder);
                asyncManager.registerCallableInterceptor(key, new SessionBindingCallableInterceptor(sessionFactory,
                        sessionHolder));
            }
        }
        try {
            filterChain.doFilter(request, response);
        } finally {
            if (!participate) {
                SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager
                        .unbindResource(sessionFactory);
                if (!isAsyncStarted(request)) {
                    logger.debug("Closing Hibernate Session in OpenSessionInViewFilter");
                    SessionFactoryUtils.closeSession(sessionHolder.getSession());
                }
            }
        }
    }

    /**
     * Look up the SessionFactory that this filter should use, taking the current HTTP request as argument.
     * <p>
     * The default implementation delegates to the {@link #lookupSessionFactory()} variant without arguments.
     * @param request the current request
     * @return the SessionFactory to use
     */
    protected final SessionFactory lookupSessionFactory(final HttpServletRequest request) {
        return lookupSessionFactory();
    }

    /**
     * Look up the SessionFactory that this filter should use.
     * <p>
     * The default implementation looks for a bean with the specified name in Spring's root application context.
     * @return the SessionFactory to use
     * @see #getSessionFactoryBeanName
     */
    protected final SessionFactory lookupSessionFactory() {
        if (logger.isDebugEnabled()) {
            logger.debug("Using SessionFactory '" + getSessionFactoryBeanName() + "' for OpenSessionInViewFilter");
        }
        WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext());
        return wac.getBean(getSessionFactoryBeanName(), SessionFactory.class);
    }

    /**
     * Open a Session for the SessionFactory that this filter uses.
     * <p>
     * The default implementation delegates to the {@code SessionFactory.openSession} method and sets the
     * {@code Session}'s flush mode to "MANUAL".
     * @param sessionFactory the SessionFactory that this filter uses
     * @return the Session to use
     * @see org.hibernate.FlushMode#MANUAL
     */
    protected final Session openSession(final SessionFactory sessionFactory) {
        Session session = sessionFactory.openSession();
        session.setFlushMode(FlushMode.AUTO);
        return session;
    }

    /**
     * @param asyncManager a.
     * @param key k.
     * @return boolean b.
     */
    private boolean applySessionBindingInterceptor(final WebAsyncManager asyncManager, final String key) {
        if (asyncManager.getCallableInterceptor(key) == null) {
            return false;
        }
        ((SessionBindingCallableInterceptor) asyncManager.getCallableInterceptor(key)).initializeThread();
        return true;
    }

    /**
     * Bind and unbind the Hibernate {@code Session} to the current thread.
     */
    private static class SessionBindingCallableInterceptor extends CallableProcessingInterceptorAdapter {
        /**
         * .
         */
        private final SessionFactory sessionFactory;
        /**
         * .
         */
        private final SessionHolder sessionHolder;

        /**
         * @param sessionFactoryt t.
         * @param sessionHoldert t.
         */
        public SessionBindingCallableInterceptor(final SessionFactory sessionFactoryt,
                final SessionHolder sessionHoldert) {
            this.sessionFactory = sessionFactoryt;
            this.sessionHolder = sessionHoldert;
        }

        @Override
        public <T> void preProcess(final NativeWebRequest request, final Callable<T> task) {
            initializeThread();
        }

        @Override
        public <T> void postProcess(final NativeWebRequest request, final Callable<T> task,
                final Object concurrentResult) {
            TransactionSynchronizationManager.unbindResource(this.sessionFactory);
        }

        /**
         * .
         */
        private void initializeThread() {
            TransactionSynchronizationManager.bindResource(this.sessionFactory, this.sessionHolder);
        }
    }
}
