Added support for Hibernate's session-per-conversation pattern (SWF-92)
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
<classpathentry kind="lib" path="lib/global/commons-logging.jar"/>
|
||||
<classpathentry kind="lib" path="lib/buildtime/concurrent.jar"/>
|
||||
<classpathentry kind="lib" path="lib/test/easymock.jar"/>
|
||||
<classpathentry kind="lib" path="lib/buildtime/hibernate.jar"/>
|
||||
<classpathentry kind="lib" path="lib/test/log4j.jar"/>
|
||||
<classpathentry kind="lib" path="lib/buildtime/junit.jar"/>
|
||||
<classpathentry kind="lib" path="lib/buildtime/myfaces-api.jar"/>
|
||||
@@ -19,11 +20,18 @@
|
||||
<classpathentry kind="lib" path="lib/global/spring-beans.jar" sourcepath="/spring/src"/>
|
||||
<classpathentry kind="lib" path="lib/global/spring-context.jar" sourcepath="/spring/src"/>
|
||||
<classpathentry kind="lib" path="lib/global/spring-core.jar" sourcepath="/spring/src"/>
|
||||
<classpathentry kind="lib" path="lib/buildtime/spring-portlet.jar"/>
|
||||
<classpathentry kind="lib" path="lib/buildtime/spring-struts.jar"/>
|
||||
<classpathentry kind="lib" path="lib/buildtime/spring-dao.jar" sourcepath="/spring/src"/>
|
||||
<classpathentry kind="lib" path="lib/buildtime/spring-hibernate3.jar" sourcepath="/spring/src"/>
|
||||
<classpathentry kind="lib" path="lib/buildtime/spring-portlet.jar" sourcepath="/spring/src"/>
|
||||
<classpathentry kind="lib" path="lib/buildtime/spring-struts.jar" sourcepath="/spring/src"/>
|
||||
<classpathentry kind="lib" path="lib/global/spring-web.jar" sourcepath="/spring/src"/>
|
||||
<classpathentry kind="lib" path="lib/buildtime/spring-webmvc.jar" sourcepath="/spring/src"/>
|
||||
<classpathentry kind="lib" path="lib/buildtime/struts.jar"/>
|
||||
<classpathentry kind="lib" path="lib/buildtime/commons-collections.jar"/>
|
||||
<classpathentry kind="lib" path="lib/buildtime/dom4j.jar"/>
|
||||
<classpathentry kind="lib" path="lib/test/hsqldb.jar"/>
|
||||
<classpathentry kind="lib" path="lib/buildtime/jta.jar"/>
|
||||
<classpathentry kind="lib" path="lib/test/spring-jdbc.jar"/>
|
||||
<classpathentry kind="lib" path="lib/test/spring-mock.jar" sourcepath="/spring/mock"/>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
||||
|
||||
@@ -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)
|
||||
-------------------------------------
|
||||
|
||||
|
||||
@@ -55,11 +55,16 @@
|
||||
<dependency org="concurrent" name="concurrent" rev="1.3.4" conf="buildtime->default" />
|
||||
<dependency org="javax.servlet" name="servlet-api" rev="2.4" conf="buildtime->default" />
|
||||
<dependency org="javax.portlet" name="portlet-api" rev="1.0" conf="buildtime->default" />
|
||||
<dependency org="org.hibernate" name="hibernate" rev="3.2.3.ga" conf="buildtime->default" />
|
||||
<dependency org="org.springframework" name="spring-dao" rev="2.0.4" conf="buildtime->default" />
|
||||
<dependency org="org.springframework" name="spring-hibernate3" rev="2.0.4" conf="buildtime->default" />
|
||||
|
||||
<!-- test time only dependencies -->
|
||||
<dependency org="hsqldb" name="hsqldb" rev="1.8.0.7" conf="test->default" />
|
||||
<dependency org="log4j" name="log4j" rev="1.2.14" conf="test->default" />
|
||||
<dependency org="org.easymock" name="easymock" rev="2.2" conf="test->default" />
|
||||
<dependency org="com.cenqua.clover" name="clover" rev="1.3.12" conf="test->default" />
|
||||
<dependency org="org.springframework" name="spring-jdbc" rev="2.0.4" conf="test->default" />
|
||||
<dependency org="org.springframework" name="spring-mock" rev="2.0.4" conf="test->default" />
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -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
|
||||
* <code>HibernateTransactionManager</code>.
|
||||
*
|
||||
* 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 <code>Session</code>
|
||||
* 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
|
||||
* <code>Session</code> 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
|
||||
* <code>Session</code> 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
|
||||
* <code>Session</code> 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE hibernate-mapping PUBLIC
|
||||
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
|
||||
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
|
||||
|
||||
<hibernate-mapping
|
||||
package="org.springframework.webflow.support.persistence"
|
||||
default-access="field" default-lazy="false">
|
||||
<class name="TestBean" table="T_BEAN">
|
||||
<id name="entityId" column="ID">
|
||||
<generator class="increment"/>
|
||||
</id>
|
||||
<property name="name" column="NAME" />
|
||||
</class>
|
||||
</hibernate-mapping>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user