/* * Copyright 2004-2012 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 javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import org.springframework.orm.jpa.EntityManagerHolder; 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.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 * standard Java Persistence API (JPA). * <p> * This implementation uses standard JPA APIs. The general pattern is as follows: * <ul> * <li>When a flow execution starts, create a new JPA persistence context and bind it to flow scope under the name * {@link #PERSISTENCE_CONTEXT_ATTRIBUTE}. * <li>Before processing a flow execution request, expose the flow-scoped persistence context as the "current" * persistence context for the current thread. * <li>When an existing flow pauses, unbind the persistence context from the current thread. * <li>When an existing flow ends, commit the changes made to the persistence context 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 conversation 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 if the underlying JPA Transaction Manager supports this. Spring's JPA TransactionManager does support * this when working with a Hibernate JPA provider, for example. In that case, Spring will handle setting the FlushMode * to MANUAL to ensure any in-progress changes to managed persistent entities are not flushed, while reads of new * objects occur transactionally. * <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 flow, triggered by reaching a 'commit' end state. * * @author Keith Donald * @author Juergen Hoeller */ public class JpaFlowExecutionListener extends FlowExecutionListenerAdapter { /** * The name of the attribute the flow {@link EntityManager persistence context} is indexed under. */ public static final String PERSISTENCE_CONTEXT_ATTRIBUTE = "persistenceContext"; private EntityManagerFactory entityManagerFactory; private TransactionTemplate transactionTemplate; /** * Create a new JPA flow execution listener using given JPA Entity Manager factory. * @param entityManagerFactory the entity manager factory to use */ public JpaFlowExecutionListener(EntityManagerFactory entityManagerFactory, PlatformTransactionManager transactionManager) { this.entityManagerFactory = entityManagerFactory; this.transactionTemplate = new TransactionTemplate(transactionManager); } public void sessionStarting(RequestContext context, FlowSession session, MutableAttributeMap<?> input) { boolean reusePersistenceContext = false; if (isParentPersistenceContext(session)) { if (isPersistenceContext(session.getDefinition())) { setEntityManager(session, getEntityManager(session.getParent())); reusePersistenceContext = true; } else { unbind(getEntityManager(session.getParent())); } } if (isPersistenceContext(session.getDefinition()) && (!reusePersistenceContext)) { EntityManager em = entityManagerFactory.createEntityManager(); session.getScope().put(PERSISTENCE_CONTEXT_ATTRIBUTE, em); bind(em); } } public void paused(RequestContext context) { if (isPersistenceContext(context.getActiveFlow())) { unbind(getEntityManager(context.getFlowExecutionContext().getActiveSession())); } } public void resuming(RequestContext context) { if (isPersistenceContext(context.getActiveFlow())) { bind(getEntityManager(context.getFlowExecutionContext().getActiveSession())); } } public void sessionEnding(RequestContext context, FlowSession session, String outcome, MutableAttributeMap<?> output) { if (isParentPersistenceContext(session)) { return; } if (isPersistenceContext(session.getDefinition())) { final EntityManager em = getEntityManager(session); Boolean commitStatus = session.getState().getAttributes().getBoolean("commit"); if (Boolean.TRUE.equals(commitStatus)) { transactionTemplate.execute(new TransactionCallbackWithoutResult() { protected void doInTransactionWithoutResult(TransactionStatus status) { em.joinTransaction(); } }); } unbind(em); em.close(); } } public void sessionEnded(RequestContext context, FlowSession session, String outcome, AttributeMap<?> output) { if (isParentPersistenceContext(session)) { if (!isPersistenceContext(session.getDefinition())) { bind(getEntityManager(session.getParent())); } } } public void exceptionThrown(RequestContext context, FlowExecutionException exception) { if (context.getFlowExecutionContext().isActive()) { if (isPersistenceContext(context.getActiveFlow())) { unbind(getEntityManager(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 EntityManager getEntityManager(FlowSession session) { return (EntityManager) session.getScope().get(PERSISTENCE_CONTEXT_ATTRIBUTE); } private void setEntityManager(FlowSession session, EntityManager em) { session.getScope().put(PERSISTENCE_CONTEXT_ATTRIBUTE, em); } private void bind(EntityManager em) { TransactionSynchronizationManager.bindResource(entityManagerFactory, new EntityManagerHolder(em)); } private void unbind(EntityManager em) { if (TransactionSynchronizationManager.hasResource(entityManagerFactory)) { TransactionSynchronizationManager.unbindResource(entityManagerFactory); } } }