001/*
002 * Copyright 2002-2018 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.jpa.support;
018
019import javax.persistence.EntityManager;
020import javax.persistence.EntityManagerFactory;
021import javax.persistence.PersistenceException;
022
023import org.springframework.dao.DataAccessException;
024import org.springframework.dao.DataAccessResourceFailureException;
025import org.springframework.lang.Nullable;
026import org.springframework.orm.jpa.EntityManagerFactoryAccessor;
027import org.springframework.orm.jpa.EntityManagerFactoryUtils;
028import org.springframework.orm.jpa.EntityManagerHolder;
029import org.springframework.transaction.support.TransactionSynchronizationManager;
030import org.springframework.ui.ModelMap;
031import org.springframework.web.context.request.AsyncWebRequestInterceptor;
032import org.springframework.web.context.request.WebRequest;
033import org.springframework.web.context.request.async.CallableProcessingInterceptor;
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 JPA EntityManager to the
039 * thread for the entire processing of the request. Intended for the "Open
040 * EntityManager in View" pattern, i.e. to allow for lazy loading in
041 * web views despite the original transactions already being completed.
042 *
043 * <p>This interceptor makes JPA EntityManagers available via the current thread,
044 * which will be autodetected by transaction managers. It is suitable for service
045 * layer transactions via {@link org.springframework.orm.jpa.JpaTransactionManager}
046 * or {@link org.springframework.transaction.jta.JtaTransactionManager} as well
047 * as for non-transactional read-only execution.
048 *
049 * <p>In contrast to {@link OpenEntityManagerInViewFilter}, this interceptor is set
050 * up in a Spring application context and can thus take advantage of bean wiring.
051 *
052 * @author Juergen Hoeller
053 * @since 2.0
054 * @see OpenEntityManagerInViewFilter
055 * @see org.springframework.orm.jpa.JpaTransactionManager
056 * @see org.springframework.orm.jpa.SharedEntityManagerCreator
057 * @see org.springframework.transaction.support.TransactionSynchronizationManager
058 */
059public class OpenEntityManagerInViewInterceptor extends EntityManagerFactoryAccessor implements AsyncWebRequestInterceptor {
060
061        /**
062         * Suffix that gets appended to the EntityManagerFactory toString
063         * representation for the "participate in existing entity manager
064         * handling" request attribute.
065         * @see #getParticipateAttributeName
066         */
067        public static final String PARTICIPATE_SUFFIX = ".PARTICIPATE";
068
069
070        @Override
071        public void preHandle(WebRequest request) throws DataAccessException {
072                String key = getParticipateAttributeName();
073                WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
074                if (asyncManager.hasConcurrentResult() && applyEntityManagerBindingInterceptor(asyncManager, key)) {
075                        return;
076                }
077
078                EntityManagerFactory emf = obtainEntityManagerFactory();
079                if (TransactionSynchronizationManager.hasResource(emf)) {
080                        // Do not modify the EntityManager: just mark the request accordingly.
081                        Integer count = (Integer) request.getAttribute(key, WebRequest.SCOPE_REQUEST);
082                        int newCount = (count != null ? count + 1 : 1);
083                        request.setAttribute(getParticipateAttributeName(), newCount, WebRequest.SCOPE_REQUEST);
084                }
085                else {
086                        logger.debug("Opening JPA EntityManager in OpenEntityManagerInViewInterceptor");
087                        try {
088                                EntityManager em = createEntityManager();
089                                EntityManagerHolder emHolder = new EntityManagerHolder(em);
090                                TransactionSynchronizationManager.bindResource(emf, emHolder);
091
092                                AsyncRequestInterceptor interceptor = new AsyncRequestInterceptor(emf, emHolder);
093                                asyncManager.registerCallableInterceptor(key, interceptor);
094                                asyncManager.registerDeferredResultInterceptor(key, interceptor);
095                        }
096                        catch (PersistenceException ex) {
097                                throw new DataAccessResourceFailureException("Could not create JPA EntityManager", ex);
098                        }
099                }
100        }
101
102        @Override
103        public void postHandle(WebRequest request, @Nullable ModelMap model) {
104        }
105
106        @Override
107        public void afterCompletion(WebRequest request, @Nullable Exception ex) throws DataAccessException {
108                if (!decrementParticipateCount(request)) {
109                        EntityManagerHolder emHolder = (EntityManagerHolder)
110                                        TransactionSynchronizationManager.unbindResource(obtainEntityManagerFactory());
111                        logger.debug("Closing JPA EntityManager in OpenEntityManagerInViewInterceptor");
112                        EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager());
113                }
114        }
115
116        private boolean decrementParticipateCount(WebRequest request) {
117                String participateAttributeName = getParticipateAttributeName();
118                Integer count = (Integer) request.getAttribute(participateAttributeName, WebRequest.SCOPE_REQUEST);
119                if (count == null) {
120                        return false;
121                }
122                // Do not modify the Session: just clear the marker.
123                if (count > 1) {
124                        request.setAttribute(participateAttributeName, count - 1, WebRequest.SCOPE_REQUEST);
125                }
126                else {
127                        request.removeAttribute(participateAttributeName, WebRequest.SCOPE_REQUEST);
128                }
129                return true;
130        }
131
132        @Override
133        public void afterConcurrentHandlingStarted(WebRequest request) {
134                if (!decrementParticipateCount(request)) {
135                        TransactionSynchronizationManager.unbindResource(obtainEntityManagerFactory());
136                }
137        }
138
139        /**
140         * Return the name of the request attribute that identifies that a request is
141         * already filtered. Default implementation takes the toString representation
142         * of the EntityManagerFactory instance and appends ".FILTERED".
143         * @see #PARTICIPATE_SUFFIX
144         */
145        protected String getParticipateAttributeName() {
146                return obtainEntityManagerFactory().toString() + PARTICIPATE_SUFFIX;
147        }
148
149
150        private boolean applyEntityManagerBindingInterceptor(WebAsyncManager asyncManager, String key) {
151                CallableProcessingInterceptor cpi = asyncManager.getCallableInterceptor(key);
152                if (cpi == null) {
153                        return false;
154                }
155                ((AsyncRequestInterceptor) cpi).bindEntityManager();
156                return true;
157        }
158
159}