diff --git a/spring-webflow/.classpath b/spring-webflow/.classpath index e92101ae..d372b1ec 100644 --- a/spring-webflow/.classpath +++ b/spring-webflow/.classpath @@ -33,5 +33,7 @@ + + diff --git a/spring-webflow/ivy.xml b/spring-webflow/ivy.xml index beb1e9ca..3ec50af5 100644 --- a/spring-webflow/ivy.xml +++ b/spring-webflow/ivy.xml @@ -55,10 +55,15 @@ - + + - + + + + + diff --git a/spring-webflow/src/main/java/org/springframework/webflow/support/persistence/HibernateSessionPerConversationListener.java b/spring-webflow/src/main/java/org/springframework/webflow/support/persistence/HibernateSessionPerConversationListener.java index ae0ba1a7..7c9e2aff 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/support/persistence/HibernateSessionPerConversationListener.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/support/persistence/HibernateSessionPerConversationListener.java @@ -23,7 +23,6 @@ import org.springframework.orm.hibernate3.SessionHolder; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.webflow.core.collection.AttributeMap; -import org.springframework.webflow.execution.FlowExecution; import org.springframework.webflow.execution.FlowExecutionException; import org.springframework.webflow.execution.FlowExecutionListener; import org.springframework.webflow.execution.FlowExecutionListenerAdapter; @@ -32,134 +31,93 @@ import org.springframework.webflow.execution.RequestContext; import org.springframework.webflow.execution.ViewSelection; /** - * A {@link FlowExecutionListener} that implements the Hibernate - * Session-per-Conversation pattern as described in Java Persistence with - * Hibernate (chapter 11). + * A {@link FlowExecutionListener} that implements the Hibernate Session-per-Conversation pattern as described in Java + * Persistence with Hibernate (chapter 11). *

- * This implementation uses raw Hibernate APIs and binds the current session to - * the thread-local location identified by Spring's - * HibernateTransactionManager. + * This implementation uses raw Hibernate APIs and binds the current session to the thread-local location identified by + * Spring's HibernateTransactionManager. *

- * This listener assumes that you are accessing Hibernate via Spring support - * such as HibernateTemplate or the LocalSessionFactoryBean. If not, Hibernate - * data access code will not participate in the proper transaction. + * This listener assumes that you are accessing Hibernate via Spring support such as HibernateTemplate or the + * LocalSessionFactoryBean. If not, Hibernate data access code will not participate in the proper transaction. *

- * Note that when accessing service layer methods with Spring managed - * transactions, those transaction should have {@link Propagation#REQUIRED} - * semantics. Anything else defeats the purpose of holding the transaction open - * until the end of the session. + * Note that when accessing service layer methods with Spring managed transactions, those transaction should have + * {@link Propagation#REQUIRED} semantics. Anything else defeats the purpose of holding the transaction open until the + * end of the session. * * @author Ben Hale * @since 1.1 */ public class HibernateSessionPerConversationListener extends FlowExecutionListenerAdapter { - private static final String HIBERNATE_SESSION = "hibernate.session"; + private static final String HIBERNATE_SESSION_ATTRIBUTE = "hibernate.session"; - private SessionFactory sessionFactory; + private SessionFactory sessionFactory; - /** - * Create a new Session-per-Conversation listener using giving Hibernate session - * factory. - * @param sessionFactory the session factory to use - */ - public HibernateSessionPerConversationListener(SessionFactory sessionFactory) { - this.sessionFactory = sessionFactory; - } + /** + * Create a new Session-per-Conversation listener using giving Hibernate session factory. + * @param sessionFactory the session factory to use + */ + public HibernateSessionPerConversationListener(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } - /** - * When a {@link FlowSession} is created a Hibernate Session - * is opened and put into MANUAL flush mode. From there it is bound to the - * conversation scope of this {@link FlowSession}. Since - * {@link #resumed(RequestContext)} will not be called on start of the - * session, this method also binds the session to the thread-local location. - * This behavior will only be exhibited on the root flow in a {@link FlowExecution}. - */ - public void sessionCreated(RequestContext context, FlowSession session) { - if (session.isRoot()) { - Session hibSession = createSession(context); - bindSession(hibSession); - } + public void sessionCreated(RequestContext context, FlowSession session) { + if (session.isRoot()) { + Session hibernateSession = createSession(context); + context.getConversationScope().put(HIBERNATE_SESSION_ATTRIBUTE, hibernateSession); + bind(hibernateSession, context); } + } - /** - * When a {@link FlowExecution} is resumed, the conversationally scoped - * Session is bound to the thread-local location and a - * transaction is opened. - */ - public void resumed(RequestContext context) { - Session hibSession = getHibernateSession(context); - bindSession(hibSession); - } + public void resumed(RequestContext context) { + Session hibSession = getHibernateSession(context); + bind(hibSession, context); + } - /** - * When a {@link FlowExecution} is paused the conversationally scoped - * Session is unbound from the thread-local location and the - * transaction is committed. The transaction that is closed is a Hibernate - * transaction and with a FlushMode of MANUAL will not flush anything to the - * database. - */ - public void paused(RequestContext context, ViewSelection selectedView) { - Session hibSession = getHibernateSession(context); - unBindSession(hibSession); - } + public void paused(RequestContext context, ViewSelection selectedView) { + Session session = getHibernateSession(context); + unbind(session, context); + } - /** - * When a {@link FlowSession} is destroyed all changes are flushed to the - * database, the current transaction committed, and the Hibernate - * Session is closed. Since - * {@link #paused(RequestContext, ViewSelection)} will not be called on the - * ending of this session, this method also unbinds the session from the - * thread-local location. This behavior will only be exhibited on the root - * flow in a {@link FlowExecution}. - */ - public void sessionEnded(RequestContext context, FlowSession session, AttributeMap output) { - if (session.isRoot()) { - Session hibSession = (Session) context.getConversationScope().remove(HIBERNATE_SESSION); - hibSession.flush(); - unBindSession(hibSession); - destroySession(hibSession); - } + public void sessionEnded(RequestContext context, FlowSession session, AttributeMap output) { + if (session.isRoot()) { + Session hibernateSession = (Session) context.getConversationScope().remove(HIBERNATE_SESSION_ATTRIBUTE); + hibernateSession.flush(); + unbind(hibernateSession, context); + hibernateSession.close(); } - - /** - * When an exception is thrown from a {@link FlowExecution}, the - * conversationally scoped Session is unbound from the - * thread-local location and the transaction is committed. The - * transaction that is closed is a Hibernate transaction and with a - * FlushMode of MANUAL will not flush anything to the database. - */ - public void exceptionThrown(RequestContext context, FlowExecutionException exception) { - Session hibSession = getHibernateSession(context); - unBindSession(hibSession); - } - - // internal helpers + } - private Session createSession(RequestContext context) { - Session hibSession = sessionFactory.openSession(); - hibSession.setFlushMode(FlushMode.MANUAL); - context.getConversationScope().put(HIBERNATE_SESSION, hibSession); - return hibSession; - } + public void exceptionThrown(RequestContext context, FlowExecutionException exception) { + Session session = getHibernateSession(context); + unbind(session, context); + } - private void destroySession(Session hibSession) { - hibSession.close(); - } + // internal helpers - private Session getHibernateSession(RequestContext context) { - return (Session) context.getConversationScope().get(HIBERNATE_SESSION); - } + private Session createSession(RequestContext context) { + Session session = sessionFactory.openSession(); + session.setFlushMode(FlushMode.MANUAL); + return session; + } - private void bindSession(Session hibSession) { - Transaction hibTx = hibSession.beginTransaction(); - SessionHolder sessionHolder = new SessionHolder(hibSession); - sessionHolder.setTransaction(hibTx); - TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder); - } + private Session getHibernateSession(RequestContext context) { + return (Session) context.getConversationScope().get(HIBERNATE_SESSION_ATTRIBUTE); + } - private void unBindSession(Session hibSession) { - hibSession.getTransaction().commit(); - TransactionSynchronizationManager.unbindResource(sessionFactory); + private void bind(Session hibSession, RequestContext context) { + SessionHolder sessionHolder = new SessionHolder(hibSession); + if (context.getActiveFlow().getAttributes().getBoolean("transactional").booleanValue() == true) { + Transaction tx = hibSession.beginTransaction(); + sessionHolder.setTransaction(tx); } -} + TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder); + } + + private void unbind(Session hibSession, RequestContext context) { + if (context.getActiveFlow().getAttributes().getBoolean("transactional").booleanValue() == true) { + hibSession.getTransaction().commit(); + } + TransactionSynchronizationManager.unbindResource(sessionFactory); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/support/persistence/JpaSessionPerConversationListener.java b/spring-webflow/src/main/java/org/springframework/webflow/support/persistence/JpaSessionPerConversationListener.java new file mode 100644 index 00000000..99f6fd45 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/support/persistence/JpaSessionPerConversationListener.java @@ -0,0 +1,117 @@ +/* + * Copyright 2004-2007 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.support.persistence; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; + +import org.springframework.orm.jpa.EntityManagerHolder; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.webflow.core.collection.AttributeMap; +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; +import org.springframework.webflow.execution.ViewSelection; + +/** + * A {@link FlowExecutionListener} that implements the Session-per-Conversation using the standard Java Persistence API + * (JPA). + *

+ * This implementation uses standard JPA APIs. The general pattern is as follows: + *

    + *
  • When a flow execution starts, create a new JPA persistence context and bind it to conversation scope. + *
  • Before processing a flow execution request, expose the conversationally-bound persistence context as the + * "current" persistence context for the current thread. + *
  • When an existing flow pauses, unbind the persistence context from the current thread. + *
  • When an existing flow ends, unbind persistence context and and close it. + *
+ * + * The general data access pattern implemented here is: + *
    + *
  • Create a new persistence context when a new conversation (e.g. edit session) starts + *
  • Load some objects non-transactionally using this persistence context + *
  • Perform edits to those objects over a series of conversational requests + *
  • On successful conversation completion, commit and flush those edits to the database + *
+ * + * Note: care should be taken to ensure at the service-layer that all data access in a conversation occurs + * non-transactionally until the final "commit" request to ensure isolation of intermediate object changes made during + * the course of the conversation. This care should be taken because, by default, JPA will always flush upon transaction + * commit, resulting in changes in the object model being synchronized with the database at that time. You would + * generally not want such intermediate flushing to happen, as the nature of a conversation implies a transient resource + * that can be canceled. + * + * @author Keith Donald + * @since 1.1 + */ +public class JpaSessionPerConversationListener extends FlowExecutionListenerAdapter { + + private static final String ENTITY_MANAGER_ATTRIBUTE = "jpa.entityManager"; + + private EntityManagerFactory entityManagerFactory; + + /** + * Create a new Session-per-Conversation listener using given JPA Entity Manager factory. + * @param entityManagerFactory the entity manager factory to use + */ + public JpaSessionPerConversationListener(EntityManagerFactory entityManagerFactory) { + this.entityManagerFactory = entityManagerFactory; + } + + public void sessionCreated(RequestContext context, FlowSession session) { + if (session.isRoot()) { + EntityManager em = entityManagerFactory.createEntityManager(); + context.getConversationScope().put(ENTITY_MANAGER_ATTRIBUTE, em); + bind(em); + } + } + + public void resumed(RequestContext context) { + bind(getEntityManager(context)); + } + + public void paused(RequestContext context, ViewSelection selectedView) { + unbind(getEntityManager(context)); + } + + public void sessionEnded(RequestContext context, FlowSession session, AttributeMap output) { + if (session.isRoot()) { + EntityManager em = (EntityManager) context.getConversationScope().remove(ENTITY_MANAGER_ATTRIBUTE); + unbind(em); + em.close(); + } + } + + public void exceptionThrown(RequestContext context, FlowExecutionException exception) { + unbind(getEntityManager(context)); + } + + // internal helpers + + private EntityManager getEntityManager(RequestContext context) { + return (EntityManager) context.getConversationScope().get(ENTITY_MANAGER_ATTRIBUTE); + } + + private void bind(EntityManager em) { + TransactionSynchronizationManager.bindResource(em, new EntityManagerHolder(em)); + } + + private void unbind(EntityManager em) { + TransactionSynchronizationManager.unbindResource(em); + } +}