From ecf3c2639ddc29dd69ce64d90e021e60cd70dd68 Mon Sep 17 00:00:00 2001 From: Ben Hale Date: Wed, 9 May 2007 14:18:23 +0000 Subject: [PATCH] Added support for Hibernate's session-per-conversation pattern (SWF-92) --- spring-webflow/.classpath | 12 +- spring-webflow/changelog.txt | 3 + spring-webflow/ivy.xml | 5 + ...bernateSessionPerConversationListener.java | 131 +++++++++++++++ ...teSessionPerConversationListenerTests.java | 155 ++++++++++++++++++ .../support/persistence/TestBean.hbm.xml | 15 ++ .../webflow/support/persistence/TestBean.java | 16 ++ 7 files changed, 335 insertions(+), 2 deletions(-) create mode 100644 spring-webflow/src/main/java/org/springframework/webflow/support/persistence/HibernateSessionPerConversationListener.java create mode 100644 spring-webflow/src/test/java/org/springframework/webflow/support/persistence/HibernateSessionPerConversationListenerTests.java create mode 100644 spring-webflow/src/test/java/org/springframework/webflow/support/persistence/TestBean.hbm.xml create mode 100644 spring-webflow/src/test/java/org/springframework/webflow/support/persistence/TestBean.java diff --git a/spring-webflow/.classpath b/spring-webflow/.classpath index c23acd06..e92101ae 100644 --- a/spring-webflow/.classpath +++ b/spring-webflow/.classpath @@ -9,6 +9,7 @@ + @@ -19,11 +20,18 @@ - - + + + + + + + + + diff --git a/spring-webflow/changelog.txt b/spring-webflow/changelog.txt index 0f384a57..c79b8bfd 100644 --- a/spring-webflow/changelog.txt +++ b/spring-webflow/changelog.txt @@ -38,6 +38,9 @@ Package org.springframework.webflow.executor * JSF integration code now binds the current FlowExecutionContext to a thread local for use by the Spring scoping mechanism (SWF-163). +Package org.springframework.webflow.support +* Added native support for Hibernate's session-per-conversation pattern (SWF-92). + Changes in version 1.0.3 (24.04.2007) ------------------------------------- diff --git a/spring-webflow/ivy.xml b/spring-webflow/ivy.xml index 84c0be5a..ce464d3d 100644 --- a/spring-webflow/ivy.xml +++ b/spring-webflow/ivy.xml @@ -55,11 +55,16 @@ + + + + + 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 new file mode 100644 index 00000000..4e5b8243 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/support/persistence/HibernateSessionPerConversationListener.java @@ -0,0 +1,131 @@ +/* + * 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 org.hibernate.FlushMode; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.springframework.orm.hibernate3.SessionHolder; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.execution.FlowExecution; +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 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 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. + * + * @author Ben Hale + * @since 1.1 + */ +public class HibernateSessionPerConversationListener extends FlowExecutionListenerAdapter { + + private static final String HIBERNATE_SESSION = "hibernate.session"; + + private SessionFactory sessionFactory; + + 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. + */ + public void sessionCreated(RequestContext context, FlowSession session) { + Session hibSession = createSession(context); + bindSession(hibSession); + } + + /** + * 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); + } + + /** + * 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); + } + + /** + * 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. + */ + public void sessionEnded(RequestContext context, FlowSession session, AttributeMap output) { + Session hibSession = (Session) context.getConversationScope().remove(HIBERNATE_SESSION); + hibSession.flush(); + unBindSession(hibSession); + destroySession(hibSession); + } + + private Session createSession(RequestContext context) { + Session hibSession = sessionFactory.openSession(); + hibSession.setFlushMode(FlushMode.MANUAL); + context.getConversationScope().put(HIBERNATE_SESSION, hibSession); + return hibSession; + } + + private void destroySession(Session hibSession) { + hibSession.close(); + } + + private Session getHibernateSession(RequestContext context) { + return (Session) context.getConversationScope().get(HIBERNATE_SESSION); + } + + private void bindSession(Session hibSession) { + hibSession.beginTransaction(); + SessionHolder sessionHolder = new SessionHolder(hibSession); + TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder); + } + + private void unBindSession(Session hibSession) { + hibSession.getTransaction().commit(); + TransactionSynchronizationManager.unbindResource(sessionFactory); + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/support/persistence/HibernateSessionPerConversationListenerTests.java b/spring-webflow/src/test/java/org/springframework/webflow/support/persistence/HibernateSessionPerConversationListenerTests.java new file mode 100644 index 00000000..e28d38a8 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/support/persistence/HibernateSessionPerConversationListenerTests.java @@ -0,0 +1,155 @@ +/* + * 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 java.sql.Connection; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import junit.framework.TestCase; + +import org.hibernate.HibernateException; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.orm.hibernate3.HibernateCallback; +import org.springframework.orm.hibernate3.HibernateTemplate; +import org.springframework.orm.hibernate3.LocalSessionFactoryBean; +import org.springframework.webflow.execution.ViewSelection; +import org.springframework.webflow.test.MockFlowSession; +import org.springframework.webflow.test.MockRequestContext; + +/** + * Tests for {@link HibernateSessionPerConversationListener} + * + * @author Ben Hale + */ +public class HibernateSessionPerConversationListenerTests extends TestCase { + + private JdbcTemplate jdbcTemplate; + + private HibernateTemplate hibernateTemplate; + + private HibernateSessionPerConversationListener listener; + + protected void setUp() throws Exception { + DataSource dataSource = getDataSource(); + populateDataBase(dataSource); + jdbcTemplate = new JdbcTemplate(dataSource); + SessionFactory sessionFactory = getSessionFactory(dataSource); + hibernateTemplate = new HibernateTemplate(sessionFactory); + hibernateTemplate.setCheckWriteOperations(false); + listener = new HibernateSessionPerConversationListener(sessionFactory); + } + + public void testSameSession() { + MockRequestContext context = new MockRequestContext(); + MockFlowSession flowSession = new MockFlowSession(); + listener.sessionCreated(context, flowSession); + + // Session created and bound to conversation + final Session hibSession = (Session) context.getConversationScope().get("hibernate.session"); + assertNotNull("Should have been populated", hibSession); + listener.paused(context, ViewSelection.NULL_VIEW); + + // Session bound to thread local variable + listener.resumed(context); + hibernateTemplate.execute(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException, SQLException { + assertSame("Should have been original instance", hibSession, session); + return null; + } + }, true); + listener.paused(context, ViewSelection.NULL_VIEW); + } + + public void testSingleState() { + assertEquals("Table should only have one row", 1, jdbcTemplate.queryForInt("select count(*) from T_BEAN")); + MockRequestContext context = new MockRequestContext(); + MockFlowSession flowSession = new MockFlowSession(); + listener.sessionCreated(context, flowSession); + + TestBean bean = new TestBean("Keith Donald"); + hibernateTemplate.save(bean); + assertEquals("Table should still only have one row", 1, jdbcTemplate.queryForInt("select count(*) from T_BEAN")); + + listener.sessionEnded(context, flowSession, null); + assertEquals("Table should only have two rows", 2, jdbcTemplate.queryForInt("select count(*) from T_BEAN")); + } + + public void testMultipleState() { + assertEquals("Table should only have one row", 1, jdbcTemplate.queryForInt("select count(*) from T_BEAN")); + MockRequestContext context = new MockRequestContext(); + MockFlowSession flowSession = new MockFlowSession(); + listener.sessionCreated(context, flowSession); + + TestBean bean1 = new TestBean("Keith Donald"); + hibernateTemplate.save(bean1); + assertEquals("Table should still only have one row", 1, jdbcTemplate.queryForInt("select count(*) from T_BEAN")); + listener.paused(context, ViewSelection.NULL_VIEW); + + listener.resumed(context); + TestBean bean2 = new TestBean("Keith Donald"); + hibernateTemplate.save(bean2); + assertEquals("Table should still only have one row", 1, jdbcTemplate.queryForInt("select count(*) from T_BEAN")); + + + listener.sessionEnded(context, flowSession, null); + assertEquals("Table should only have two rows", 3, jdbcTemplate.queryForInt("select count(*) from T_BEAN")); + } + + private DataSource getDataSource() { + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); + dataSource.setUrl("jdbc:hsqldb:mem:hspcl"); + dataSource.setUsername("sa"); + dataSource.setPassword(""); + return dataSource; + } + + private void populateDataBase(DataSource dataSource) { + Connection connection = null; + try { + connection = dataSource.getConnection(); + connection.createStatement().execute("drop table T_BEAN if exists;"); + connection.createStatement().execute( + "create table T_BEAN (ID integer primary key, NAME varchar(50) not null);"); + connection.createStatement().execute("insert into T_BEAN (ID, NAME) values (0, 'Ben Hale');"); + } catch (SQLException e) { + throw new RuntimeException("SQL exception occurred acquiring connection", e); + } finally { + if (connection != null) { + try { + connection.close(); + } catch (SQLException e) { + } + } + } + } + + private SessionFactory getSessionFactory(DataSource dataSource) throws Exception { + LocalSessionFactoryBean factory = new LocalSessionFactoryBean(); + factory.setDataSource(dataSource); + factory.setMappingLocations(new Resource[] { new ClassPathResource( + "org/springframework/webflow/support/persistence/TestBean.hbm.xml") }); + factory.afterPropertiesSet(); + return (SessionFactory) factory.getObject(); + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/support/persistence/TestBean.hbm.xml b/spring-webflow/src/test/java/org/springframework/webflow/support/persistence/TestBean.hbm.xml new file mode 100644 index 00000000..6fe806b7 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/support/persistence/TestBean.hbm.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/support/persistence/TestBean.java b/spring-webflow/src/test/java/org/springframework/webflow/support/persistence/TestBean.java new file mode 100644 index 00000000..de53b624 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/support/persistence/TestBean.java @@ -0,0 +1,16 @@ +package org.springframework.webflow.support.persistence; + +public class TestBean { + + private long entityId; + + private String name; + + public TestBean(String name) { + this.name = name; + } + + public String getName() { + return name; + } +}