/* * Copyright 2004-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.webflow.persistence; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import org.hibernate.FlushMode; import org.hibernate.Interceptor; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.springframework.orm.hibernate4.SessionHolder; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.transaction.support.TransactionTemplate; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; import org.springframework.webflow.core.collection.AttributeMap; import org.springframework.webflow.core.collection.MutableAttributeMap; import org.springframework.webflow.definition.FlowDefinition; import org.springframework.webflow.execution.FlowExecutionException; import org.springframework.webflow.execution.FlowExecutionListener; import org.springframework.webflow.execution.FlowExecutionListenerAdapter; import org.springframework.webflow.execution.FlowSession; import org.springframework.webflow.execution.RequestContext; /** * A {@link FlowExecutionListener} that implements the Flow Managed Persistence Context (FMPC) pattern using the native * Hibernate API. * <p> * The general pattern is as follows: * <ul> * <li>When a flow execution starts, create a new Hibernate Session and bind it to flow scope under the name * {@link #PERSISTENCE_CONTEXT_ATTRIBUTE}. * <li>Before processing a flow execution request, expose the conversationally-bound session as the "current session" * for the current thread. * <li>When an existing flow pauses, unbind the session from the current thread. * <li>When an existing flow ends, commit the changes made to the session in a transaction if the ending state is a * commit state. Then, unbind the context and close it. * </ul> * * The general data access pattern implemented here is: * <ul> * <li>Create a new persistence context when a new flow execution with the 'persistenceContext' attribute starts * <li>Load some objects into this persistence context * <li>Perform edits to those objects over a series of requests into the flow * <li>On successful flow completion, commit and flush those edits to the database, applying a version check if * necessary. * </ul> * * <p> * Note: All data access except for the final commit will, by default, be non-transactional. However, a flow may call * into a transactional service layer to fetch objects during the conversation in the context of a read-only system * transaction. In that case, the session's flush mode will be set to Manual and no intermediate changes will be * flushed. * <p> * Care should be taken to prevent premature commits of flow data while the flow is in progress. You would generally not * want intermediate flushing to happen, as the nature of a flow implies a transient, isolated resource that can be * canceled before it ends. Generally, the only time a read-write transaction should be started is upon successful * completion of the conversation, triggered by reaching a 'commit' end state. * * @author Keith Donald * @author Juergen Hoeller * @author Ben Hale */ public class HibernateFlowExecutionListener extends FlowExecutionListenerAdapter { private static final boolean hibernate3Present = ClassUtils.isPresent("org.hibernate.connection.ConnectionProvider", HibernateFlowExecutionListener.class.getClassLoader()); private static final boolean hibernate5Present = ClassUtils.isPresent("org.hibernate.boot.model.naming.PhysicalNamingStrategy", HibernateFlowExecutionListener.class.getClassLoader()); private static final Method openSessionMethod = ReflectionUtils.findMethod(SessionFactory.class, "openSession"); private static final Method openSessionWithInterceptorMethod = ReflectionUtils.findMethod(SessionFactory.class, "openSession", Interceptor.class); private static final Method currentSessionMethod = ClassUtils.getMethod(SessionFactory.class, "getCurrentSession"); private static final Method closeSessionMethod = ReflectionUtils.findMethod(Session.class, "close"); /** * The name of the attribute the flow {@link Session persistence context} is indexed under. */ public static final String PERSISTENCE_CONTEXT_ATTRIBUTE = "persistenceContext"; private SessionFactory sessionFactory; private TransactionTemplate transactionTemplate; private Interceptor entityInterceptor; /** * Create a new Hibernate Flow Execution Listener using giving Hibernate session factory and transaction manager. * @param sessionFactory the session factory to use * @param transactionManager the transaction manager to drive transactions */ public HibernateFlowExecutionListener(SessionFactory sessionFactory, PlatformTransactionManager transactionManager) { this.sessionFactory = sessionFactory; this.transactionTemplate = new TransactionTemplate(transactionManager); } /** * Sets the entity interceptor to attach to sessions opened by this listener. * @param entityInterceptor the entity interceptor */ public void setEntityInterceptor(Interceptor entityInterceptor) { this.entityInterceptor = entityInterceptor; } public void sessionStarting(RequestContext context, FlowSession session, MutableAttributeMap<?> input) { boolean reusePersistenceContext = false; if (isParentPersistenceContext(session)) { if (isPersistenceContext(session.getDefinition())) { setHibernateSession(session, getHibernateSession(session.getParent())); reusePersistenceContext = true; } else { unbind(getHibernateSession(session.getParent())); } } if (isPersistenceContext(session.getDefinition()) && (!reusePersistenceContext)) { Session hibernateSession = createSession(context); setHibernateSession(session, hibernateSession); bind(hibernateSession); } } public void paused(RequestContext context) { if (isPersistenceContext(context.getActiveFlow())) { Session session = getHibernateSession(context.getFlowExecutionContext().getActiveSession()); unbind(session); session.disconnect(); } } public void resuming(RequestContext context) { if (isPersistenceContext(context.getActiveFlow())) { bind(getHibernateSession(context.getFlowExecutionContext().getActiveSession())); } } public void sessionEnding(RequestContext context, FlowSession session, String outcome, MutableAttributeMap<?> output) { if (isParentPersistenceContext(session)) { return; } if (isPersistenceContext(session.getDefinition())) { final Session hibernateSession = getHibernateSession(session); Boolean commitStatus = session.getState().getAttributes().getBoolean("commit"); if (Boolean.TRUE.equals(commitStatus)) { transactionTemplate.execute(new TransactionCallbackWithoutResult() { protected void doInTransactionWithoutResult(TransactionStatus status) { if (hibernate3Present) { ReflectionUtils.invokeMethod(currentSessionMethod, sessionFactory); } else { sessionFactory.getCurrentSession(); } // nothing to do; a flush will happen on commit automatically as this is a read-write // transaction } }); } unbind(hibernateSession); ReflectionUtils.invokeMethod(closeSessionMethod, hibernateSession); } } public void sessionEnded(RequestContext context, FlowSession session, String outcome, AttributeMap<?> output) { if (isParentPersistenceContext(session)) { if (!isPersistenceContext(session.getDefinition())) { bind(getHibernateSession(session.getParent())); } } } public void exceptionThrown(RequestContext context, FlowExecutionException exception) { if (context.getFlowExecutionContext().isActive()) { if (isPersistenceContext(context.getActiveFlow())) { unbind(getHibernateSession(context.getFlowExecutionContext().getActiveSession())); } } } // internal helpers private boolean isPersistenceContext(FlowDefinition flow) { return flow.getAttributes().contains(PERSISTENCE_CONTEXT_ATTRIBUTE); } private boolean isParentPersistenceContext(FlowSession flowSession) { return ((!flowSession.isRoot()) && isPersistenceContext(flowSession.getParent().getDefinition())); } private Session createSession(RequestContext context) { Session session; if (entityInterceptor != null) { if (hibernate3Present) { try { session = (Session) openSessionWithInterceptorMethod.invoke(sessionFactory, entityInterceptor); } catch (IllegalAccessException ex) { throw new IllegalStateException("Unable to open Hibernate 3 session", ex); } catch (InvocationTargetException ex) { throw new IllegalStateException("Unable to open Hibernate 3 session", ex); } } else { session = sessionFactory.withOptions().interceptor(entityInterceptor).openSession(); } } else { if (hibernate3Present) { try { session = (Session) openSessionMethod.invoke(sessionFactory); } catch (IllegalAccessException ex) { throw new IllegalStateException("Unable to open Hibernate 3 session", ex); } catch (InvocationTargetException ex) { throw new IllegalStateException("Unable to open Hibernate 3 session", ex); } } else { session = sessionFactory.openSession(); } } session.setFlushMode(FlushMode.MANUAL); return session; } private Session getHibernateSession(FlowSession session) { return (Session) session.getScope().get(PERSISTENCE_CONTEXT_ATTRIBUTE); } private void setHibernateSession(FlowSession session, Session hibernateSession) { session.getScope().put(PERSISTENCE_CONTEXT_ATTRIBUTE, hibernateSession); } private void bind(Session session) { Object sessionHolder; if (hibernate3Present) { sessionHolder = new org.springframework.orm.hibernate3.SessionHolder(session); } else if (hibernate5Present) { sessionHolder = new org.springframework.orm.hibernate5.SessionHolder(session); } else { sessionHolder = new SessionHolder(session); } TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder); } private void unbind(Session session) { if (TransactionSynchronizationManager.hasResource(sessionFactory)) { TransactionSynchronizationManager.unbindResource(sessionFactory); } } }