001/*
002 * Copyright 2002-2017 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.orm.hibernate4.support;
018
019import org.apache.commons.logging.Log;
020import org.apache.commons.logging.LogFactory;
021import org.hibernate.FlushMode;
022import org.hibernate.HibernateException;
023import org.hibernate.Session;
024import org.hibernate.SessionFactory;
025
026import org.springframework.dao.DataAccessException;
027import org.springframework.dao.DataAccessResourceFailureException;
028import org.springframework.orm.hibernate4.SessionFactoryUtils;
029import org.springframework.orm.hibernate4.SessionHolder;
030import org.springframework.transaction.support.TransactionSynchronizationManager;
031import org.springframework.ui.ModelMap;
032import org.springframework.web.context.request.AsyncWebRequestInterceptor;
033import org.springframework.web.context.request.WebRequest;
034import org.springframework.web.context.request.async.WebAsyncManager;
035import org.springframework.web.context.request.async.WebAsyncUtils;
036
037/**
038 * Spring web request interceptor that binds a Hibernate {@code Session} to the
039 * thread for the entire processing of the request.
040 *
041 * <p>This class is a concrete expression of the "Open Session in View" pattern, which
042 * is a pattern that allows for the lazy loading of associations in web views despite
043 * the original transactions already being completed.
044 *
045 * <p>This interceptor makes Hibernate Sessions available via the current thread,
046 * which will be autodetected by transaction managers. It is suitable for service layer
047 * transactions via {@link org.springframework.orm.hibernate4.HibernateTransactionManager}
048 * as well as for non-transactional execution (if configured appropriately).
049 *
050 * <p>In contrast to {@link OpenSessionInViewFilter}, this interceptor is configured
051 * in a Spring application context and can thus take advantage of bean wiring.
052 *
053 * <p><b>WARNING:</b> Applying this interceptor to existing logic can cause issues
054 * that have not appeared before, through the use of a single Hibernate
055 * {@code Session} for the processing of an entire request. In particular, the
056 * reassociation of persistent objects with a Hibernate {@code Session} has to
057 * occur at the very beginning of request processing, to avoid clashes with already
058 * loaded instances of the same objects.
059 *
060 * @author Juergen Hoeller
061 * @since 3.1
062 * @see OpenSessionInViewFilter
063 * @see OpenSessionInterceptor
064 * @see org.springframework.orm.hibernate4.HibernateTransactionManager
065 * @see org.springframework.transaction.support.TransactionSynchronizationManager
066 * @see org.hibernate.SessionFactory#getCurrentSession()
067 */
068public class OpenSessionInViewInterceptor implements AsyncWebRequestInterceptor {
069
070        /**
071         * Suffix that gets appended to the {@code SessionFactory}
072         * {@code toString()} representation for the "participate in existing
073         * session handling" request attribute.
074         * @see #getParticipateAttributeName
075         */
076        public static final String PARTICIPATE_SUFFIX = ".PARTICIPATE";
077
078        protected final Log logger = LogFactory.getLog(getClass());
079
080        private SessionFactory sessionFactory;
081
082
083        /**
084         * Set the Hibernate SessionFactory that should be used to create Hibernate Sessions.
085         */
086        public void setSessionFactory(SessionFactory sessionFactory) {
087                this.sessionFactory = sessionFactory;
088        }
089
090        /**
091         * Return the Hibernate SessionFactory that should be used to create Hibernate Sessions.
092         */
093        public SessionFactory getSessionFactory() {
094                return this.sessionFactory;
095        }
096
097
098        /**
099         * Open a new Hibernate {@code Session} according and bind it to the thread via the
100         * {@link org.springframework.transaction.support.TransactionSynchronizationManager}.
101         */
102        @Override
103        public void preHandle(WebRequest request) throws DataAccessException {
104                String participateAttributeName = getParticipateAttributeName();
105
106                WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
107                if (asyncManager.hasConcurrentResult()) {
108                        if (applySessionBindingInterceptor(asyncManager, participateAttributeName)) {
109                                return;
110                        }
111                }
112
113                if (TransactionSynchronizationManager.hasResource(getSessionFactory())) {
114                        // Do not modify the Session: just mark the request accordingly.
115                        Integer count = (Integer) request.getAttribute(participateAttributeName, WebRequest.SCOPE_REQUEST);
116                        int newCount = (count != null ? count + 1 : 1);
117                        request.setAttribute(getParticipateAttributeName(), newCount, WebRequest.SCOPE_REQUEST);
118                }
119                else {
120                        logger.debug("Opening Hibernate Session in OpenSessionInViewInterceptor");
121                        Session session = openSession();
122                        SessionHolder sessionHolder = new SessionHolder(session);
123                        TransactionSynchronizationManager.bindResource(getSessionFactory(), sessionHolder);
124
125                        AsyncRequestInterceptor asyncRequestInterceptor =
126                                        new AsyncRequestInterceptor(getSessionFactory(), sessionHolder);
127                        asyncManager.registerCallableInterceptor(participateAttributeName, asyncRequestInterceptor);
128                        asyncManager.registerDeferredResultInterceptor(participateAttributeName, asyncRequestInterceptor);
129                }
130        }
131
132        @Override
133        public void postHandle(WebRequest request, ModelMap model) {
134        }
135
136        /**
137         * Unbind the Hibernate {@code Session} from the thread and close it).
138         * @see org.springframework.transaction.support.TransactionSynchronizationManager
139         */
140        @Override
141        public void afterCompletion(WebRequest request, Exception ex) throws DataAccessException {
142                if (!decrementParticipateCount(request)) {
143                        SessionHolder sessionHolder =
144                                        (SessionHolder) TransactionSynchronizationManager.unbindResource(getSessionFactory());
145                        logger.debug("Closing Hibernate Session in OpenSessionInViewInterceptor");
146                        SessionFactoryUtils.closeSession(sessionHolder.getSession());
147                }
148        }
149
150        private boolean decrementParticipateCount(WebRequest request) {
151                String participateAttributeName = getParticipateAttributeName();
152                Integer count = (Integer) request.getAttribute(participateAttributeName, WebRequest.SCOPE_REQUEST);
153                if (count == null) {
154                        return false;
155                }
156                // Do not modify the Session: just clear the marker.
157                if (count > 1) {
158                        request.setAttribute(participateAttributeName, count - 1, WebRequest.SCOPE_REQUEST);
159                }
160                else {
161                        request.removeAttribute(participateAttributeName, WebRequest.SCOPE_REQUEST);
162                }
163                return true;
164        }
165
166        @Override
167        public void afterConcurrentHandlingStarted(WebRequest request) {
168                if (!decrementParticipateCount(request)) {
169                        TransactionSynchronizationManager.unbindResource(getSessionFactory());
170                }
171        }
172
173        /**
174         * Open a Session for the SessionFactory that this interceptor uses.
175         * <p>The default implementation delegates to the {@link SessionFactory#openSession}
176         * method and sets the {@link Session}'s flush mode to "MANUAL".
177         * @return the Session to use
178         * @throws DataAccessResourceFailureException if the Session could not be created
179         * @see org.hibernate.FlushMode#MANUAL
180         */
181        protected Session openSession() throws DataAccessResourceFailureException {
182                try {
183                        Session session = getSessionFactory().openSession();
184                        session.setFlushMode(FlushMode.MANUAL);
185                        return session;
186                }
187                catch (HibernateException ex) {
188                        throw new DataAccessResourceFailureException("Could not open Hibernate Session", ex);
189                }
190        }
191
192        /**
193         * Return the name of the request attribute that identifies that a request is
194         * already intercepted.
195         * <p>The default implementation takes the {@code toString()} representation
196         * of the {@code SessionFactory} instance and appends {@link #PARTICIPATE_SUFFIX}.
197         */
198        protected String getParticipateAttributeName() {
199                return getSessionFactory().toString() + PARTICIPATE_SUFFIX;
200        }
201
202        private boolean applySessionBindingInterceptor(WebAsyncManager asyncManager, String key) {
203                if (asyncManager.getCallableInterceptor(key) == null) {
204                        return false;
205                }
206                ((AsyncRequestInterceptor) asyncManager.getCallableInterceptor(key)).bindSession();
207                return true;
208        }
209
210}