diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/EvaluationAttempt.java b/spring-binding/src/main/java/org/springframework/binding/expression/EvaluationAttempt.java index a36696ff..0a133b7f 100644 --- a/spring-binding/src/main/java/org/springframework/binding/expression/EvaluationAttempt.java +++ b/spring-binding/src/main/java/org/springframework/binding/expression/EvaluationAttempt.java @@ -15,6 +15,8 @@ */ package org.springframework.binding.expression; +import java.io.Serializable; + import org.springframework.core.style.ToStringCreator; /** @@ -22,7 +24,7 @@ import org.springframework.core.style.ToStringCreator; * * @author Keith Donald */ -public class EvaluationAttempt { +public class EvaluationAttempt implements Serializable { private Expression expression; diff --git a/spring-webflow/src/main/java/org/springframework/webflow/persistence/HibernateFlowExecutionListener.java b/spring-webflow/src/main/java/org/springframework/webflow/persistence/HibernateFlowExecutionListener.java index dcd09985..b6fe077b 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/persistence/HibernateFlowExecutionListener.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/persistence/HibernateFlowExecutionListener.java @@ -105,6 +105,12 @@ public class HibernateFlowExecutionListener extends FlowExecutionListenerAdapter public void sessionStarting(RequestContext context, FlowSession session, MutableAttributeMap input) { if (isPersistenceContext(session.getDefinition())) { + if (!session.isRoot()) { + FlowSession parent = session.getParent(); + if (isPersistenceContext(parent.getDefinition())) { + unbind(getHibernateSession(parent)); + } + } Session hibernateSession = createSession(context); session.getScope().put(HIBERNATE_SESSION_ATTRIBUTE, hibernateSession); bind(hibernateSession); @@ -113,7 +119,7 @@ public class HibernateFlowExecutionListener extends FlowExecutionListenerAdapter public void paused(RequestContext context) { if (isPersistenceContext(context.getActiveFlow())) { - Session session = getHibernateSession(context); + Session session = getHibernateSession(context.getFlowExecutionContext().getActiveSession()); unbind(session); session.disconnect(); } @@ -121,7 +127,7 @@ public class HibernateFlowExecutionListener extends FlowExecutionListenerAdapter public void resuming(RequestContext context) { if (isPersistenceContext(context.getActiveFlow())) { - bind(getHibernateSession(context)); + bind(getHibernateSession(context.getFlowExecutionContext().getActiveSession())); } } @@ -130,7 +136,6 @@ public class HibernateFlowExecutionListener extends FlowExecutionListenerAdapter final Session hibernateSession = (Session) session.getScope().remove(HIBERNATE_SESSION_ATTRIBUTE); Boolean commitStatus = session.getState().getAttributes().getBoolean("commit"); if (Boolean.TRUE.equals(commitStatus)) { - // this is a commit end state - start a new transaction that quickly commits transactionTemplate.execute(new TransactionCallbackWithoutResult() { protected void doInTransactionWithoutResult(TransactionStatus status) { sessionFactory.getCurrentSession(); @@ -142,12 +147,18 @@ public class HibernateFlowExecutionListener extends FlowExecutionListenerAdapter unbind(hibernateSession); hibernateSession.close(); } + if (!session.isRoot()) { + FlowSession parent = session.getParent(); + if (isPersistenceContext(parent.getDefinition())) { + bind(getHibernateSession(parent)); + } + } } public void exceptionThrown(RequestContext context, FlowExecutionException exception) { if (context.getFlowExecutionContext().isActive()) { if (isPersistenceContext(context.getActiveFlow())) { - unbind(getHibernateSession(context)); + unbind(getHibernateSession(context.getFlowExecutionContext().getActiveSession())); } } } @@ -165,8 +176,8 @@ public class HibernateFlowExecutionListener extends FlowExecutionListenerAdapter return session; } - private Session getHibernateSession(RequestContext context) { - return (Session) context.getFlowScope().get(HIBERNATE_SESSION_ATTRIBUTE); + private Session getHibernateSession(FlowSession session) { + return (Session) session.getScope().get(HIBERNATE_SESSION_ATTRIBUTE); } private void bind(Session session) { diff --git a/spring-webflow/src/main/java/org/springframework/webflow/persistence/JpaFlowExecutionListener.java b/spring-webflow/src/main/java/org/springframework/webflow/persistence/JpaFlowExecutionListener.java index 4bf45e84..03ad3674 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/persistence/JpaFlowExecutionListener.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/persistence/JpaFlowExecutionListener.java @@ -95,6 +95,12 @@ public class JpaFlowExecutionListener extends FlowExecutionListenerAdapter { public void sessionStarting(RequestContext context, FlowSession session, MutableAttributeMap input) { if (isPersistenceContext(session.getDefinition())) { + if (!session.isRoot()) { + FlowSession parent = session.getParent(); + if (isPersistenceContext(parent.getDefinition())) { + unbind(getEntityManager(parent)); + } + } EntityManager em = entityManagerFactory.createEntityManager(); session.getScope().put(ENTITY_MANAGER_ATTRIBUTE, em); bind(em); @@ -103,13 +109,13 @@ public class JpaFlowExecutionListener extends FlowExecutionListenerAdapter { public void paused(RequestContext context) { if (isPersistenceContext(context.getActiveFlow())) { - unbind(getEntityManager(context)); + unbind(getEntityManager(context.getFlowExecutionContext().getActiveSession())); } } public void resuming(RequestContext context) { if (isPersistenceContext(context.getActiveFlow())) { - bind(getEntityManager(context)); + bind(getEntityManager(context.getFlowExecutionContext().getActiveSession())); } } @@ -118,7 +124,6 @@ public class JpaFlowExecutionListener extends FlowExecutionListenerAdapter { final EntityManager em = (EntityManager) session.getScope().remove(ENTITY_MANAGER_ATTRIBUTE); Boolean commitStatus = session.getState().getAttributes().getBoolean("commit"); if (Boolean.TRUE.equals(commitStatus)) { - // this is a commit end state - start a new transaction that quickly commits transactionTemplate.execute(new TransactionCallbackWithoutResult() { protected void doInTransactionWithoutResult(TransactionStatus status) { em.joinTransaction(); @@ -128,12 +133,18 @@ public class JpaFlowExecutionListener extends FlowExecutionListenerAdapter { unbind(em); em.close(); } + if (!session.isRoot()) { + FlowSession parent = session.getParent(); + if (isPersistenceContext(parent.getDefinition())) { + bind(getEntityManager(parent)); + } + } } public void exceptionThrown(RequestContext context, FlowExecutionException exception) { if (context.getFlowExecutionContext().isActive()) { if (isPersistenceContext(context.getActiveFlow())) { - unbind(getEntityManager(context)); + unbind(getEntityManager(context.getFlowExecutionContext().getActiveSession())); } } } @@ -144,8 +155,8 @@ public class JpaFlowExecutionListener extends FlowExecutionListenerAdapter { return flow.getAttributes().contains(PERSISTENCE_CONTEXT_ATTRIBUTE); } - private EntityManager getEntityManager(RequestContext context) { - return (EntityManager) context.getFlowScope().get(ENTITY_MANAGER_ATTRIBUTE); + private EntityManager getEntityManager(FlowSession session) { + return (EntityManager) session.getScope().get(ENTITY_MANAGER_ATTRIBUTE); } private void bind(EntityManager em) { diff --git a/spring-webflow/src/test/java/org/springframework/webflow/persistence/HibernateFlowExecutionListenerTests.java b/spring-webflow/src/test/java/org/springframework/webflow/persistence/HibernateFlowExecutionListenerTests.java index a4ea6209..be3a9566 100644 --- a/spring-webflow/src/test/java/org/springframework/webflow/persistence/HibernateFlowExecutionListenerTests.java +++ b/spring-webflow/src/test/java/org/springframework/webflow/persistence/HibernateFlowExecutionListenerTests.java @@ -84,12 +84,12 @@ public class HibernateFlowExecutionListenerTests extends TestCase { hibernateListener.resuming(context); assertSessionBound(); - hibernateTemplate.execute(new HibernateCallback() { + hibernateTemplate.executeWithNativeSession(new HibernateCallback() { public Object doInHibernate(Session session) throws HibernateException, SQLException { assertSame("Should have been original instance", hibSession, session); return null; } - }, true); + }); hibernateListener.paused(context); assertSessionNotBound(); } diff --git a/spring-webflow/src/test/java/org/springframework/webflow/persistence/HibernateFlowManagedPersistenceIntegrationTests.java b/spring-webflow/src/test/java/org/springframework/webflow/persistence/HibernateFlowManagedPersistenceIntegrationTests.java new file mode 100644 index 00000000..0141cd48 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/persistence/HibernateFlowManagedPersistenceIntegrationTests.java @@ -0,0 +1,142 @@ +package org.springframework.webflow.persistence; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import junit.framework.TestCase; + +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.HibernateTemplate; +import org.springframework.orm.hibernate3.HibernateTransactionManager; +import org.springframework.orm.hibernate3.LocalSessionFactoryBean; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.builder.FlowAssembler; +import org.springframework.webflow.engine.builder.model.FlowModelFlowBuilder; +import org.springframework.webflow.engine.impl.FlowExecutionImplFactory; +import org.springframework.webflow.engine.model.builder.xml.XmlFlowModelBuilder; +import org.springframework.webflow.engine.model.registry.DefaultFlowModelHolder; +import org.springframework.webflow.execution.Action; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.factory.StaticFlowExecutionListenerLoader; +import org.springframework.webflow.test.MockExternalContext; +import org.springframework.webflow.test.MockFlowBuilderContext; + +public class HibernateFlowManagedPersistenceIntegrationTests extends TestCase { + + private SessionFactory sessionFactory; + + private JdbcTemplate jdbcTemplate; + + private HibernateTemplate hibernateTemplate; + + private HibernateFlowExecutionListener hibernateListener; + + private FlowExecution flowExecution; + + protected void setUp() throws Exception { + DataSource dataSource = getDataSource(); + populateDataBase(dataSource); + jdbcTemplate = new JdbcTemplate(dataSource); + sessionFactory = getSessionFactory(dataSource); + hibernateTemplate = new HibernateTemplate(sessionFactory); + hibernateTemplate.setCheckWriteOperations(false); + HibernateTransactionManager tm = new HibernateTransactionManager(sessionFactory); + hibernateListener = new HibernateFlowExecutionListener(sessionFactory, tm); + + ClassPathResource res = new ClassPathResource("flow-managed-persistence.xml", getClass()); + DefaultFlowModelHolder holder = new DefaultFlowModelHolder(new XmlFlowModelBuilder(res)); + FlowModelFlowBuilder builder = new FlowModelFlowBuilder(holder); + MockFlowBuilderContext context = new MockFlowBuilderContext("foo"); + FlowAssembler assembler = new FlowAssembler(builder, context); + Flow flow = assembler.assembleFlow(); + context.registerSubflow(flow); + context.registerBean("loadTestBean", new Action() { + public Event execute(RequestContext context) throws Exception { + assertSessionBound(); + Session session = (Session) context.getFlowScope().get("session"); + TestBean bean = (TestBean) session.get(TestBean.class, new Long(0)); + assertNotNull(bean); + context.getFlowScope().put("testBean", bean); + return new Event(this, "success"); + } + }); + FlowExecutionImplFactory factory = new FlowExecutionImplFactory(); + factory.setExecutionListenerLoader(new StaticFlowExecutionListenerLoader(hibernateListener)); + flowExecution = factory.createFlowExecution(flow); + } + + public void testFlowWithSubflow() { + MockExternalContext context = new MockExternalContext(); + flowExecution.start(null, context); + context.setEventId("subflow"); + flowExecution.resume(context); + context.setEventId("finish"); + flowExecution.resume(context); + } + + private DataSource getDataSource() { + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); + dataSource.setUrl("jdbc:hsqldb:mem:jpa"); + 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_ADDRESS if exists;"); + 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( + "create table T_ADDRESS (ID integer primary key, BEAN_ID integer, VALUE varchar(50) not null);"); + connection + .createStatement() + .execute( + "alter table T_ADDRESS add constraint FK_BEAN_ADDRESS foreign key (BEAN_ID) references T_BEAN(ID) on delete cascade"); + connection.createStatement().execute("insert into T_BEAN (ID, NAME) values (0, 'Ben Hale');"); + connection.createStatement().execute( + "insert into T_ADDRESS (ID, BEAN_ID, VALUE) values (0, 0, 'Melbourne')"); + } 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/persistence/TestBean.hbm.xml"), + new ClassPathResource("org/springframework/webflow/persistence/TestAddress.hbm.xml") }); + factory.afterPropertiesSet(); + return (SessionFactory) factory.getObject(); + } + + private void assertSessionNotBound() { + assertNull(TransactionSynchronizationManager.getResource(sessionFactory)); + } + + private void assertSessionBound() { + assertNotNull(TransactionSynchronizationManager.getResource(sessionFactory)); + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/persistence/JpaFlowManagedPersistenceIntegrationTests.java b/spring-webflow/src/test/java/org/springframework/webflow/persistence/JpaFlowManagedPersistenceIntegrationTests.java new file mode 100644 index 00000000..fae40486 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/persistence/JpaFlowManagedPersistenceIntegrationTests.java @@ -0,0 +1,141 @@ +package org.springframework.webflow.persistence; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.sql.DataSource; + +import junit.framework.TestCase; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.orm.jpa.JpaTemplate; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.OpenJpaVendorAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.builder.FlowAssembler; +import org.springframework.webflow.engine.builder.model.FlowModelFlowBuilder; +import org.springframework.webflow.engine.impl.FlowExecutionImplFactory; +import org.springframework.webflow.engine.model.builder.xml.XmlFlowModelBuilder; +import org.springframework.webflow.engine.model.registry.DefaultFlowModelHolder; +import org.springframework.webflow.execution.Action; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.factory.StaticFlowExecutionListenerLoader; +import org.springframework.webflow.test.MockExternalContext; +import org.springframework.webflow.test.MockFlowBuilderContext; + +public class JpaFlowManagedPersistenceIntegrationTests extends TestCase { + + private EntityManagerFactory entityManagerFactory; + + private JpaFlowExecutionListener jpaListener; + + private JdbcTemplate jdbcTemplate; + + private JpaTemplate jpaTemplate; + + private FlowExecution flowExecution; + + protected void setUp() throws Exception { + DataSource dataSource = getDataSource(); + populateDataBase(dataSource); + jdbcTemplate = new JdbcTemplate(dataSource); + entityManagerFactory = getEntityManagerFactory(dataSource); + JpaTransactionManager tm = new JpaTransactionManager(entityManagerFactory); + jpaListener = new JpaFlowExecutionListener(entityManagerFactory, tm); + jpaTemplate = new JpaTemplate(entityManagerFactory); + + ClassPathResource res = new ClassPathResource("flow-managed-persistence.xml", getClass()); + DefaultFlowModelHolder holder = new DefaultFlowModelHolder(new XmlFlowModelBuilder(res)); + FlowModelFlowBuilder builder = new FlowModelFlowBuilder(holder); + MockFlowBuilderContext context = new MockFlowBuilderContext("foo"); + FlowAssembler assembler = new FlowAssembler(builder, context); + Flow flow = assembler.assembleFlow(); + context.registerSubflow(flow); + context.registerBean("loadTestBean", new Action() { + public Event execute(RequestContext context) throws Exception { + assertSessionBound(); + EntityManager em = (EntityManager) context.getFlowScope().get("entityManager"); + TestBean bean = (TestBean) em.getReference(TestBean.class, new Integer(0)); + assertNotNull(bean); + context.getFlowScope().put("testBean", bean); + return new Event(this, "success"); + } + }); + FlowExecutionImplFactory factory = new FlowExecutionImplFactory(); + factory.setExecutionListenerLoader(new StaticFlowExecutionListenerLoader(jpaListener)); + flowExecution = factory.createFlowExecution(flow); + } + + public void testFlowWithSubflow() { + MockExternalContext context = new MockExternalContext(); + flowExecution.start(null, context); + context.setEventId("subflow"); + flowExecution.resume(context); + context.setEventId("finish"); + flowExecution.resume(context); + } + + private DataSource getDataSource() { + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); + dataSource.setUrl("jdbc:hsqldb:mem:jpa"); + 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_ADDRESS if exists;"); + 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( + "create table T_ADDRESS (ID integer primary key, BEAN_ID integer, VALUE varchar(50) not null);"); + connection + .createStatement() + .execute( + "alter table T_ADDRESS add constraint FK_BEAN_ADDRESS foreign key (BEAN_ID) references T_BEAN(ID) on delete cascade"); + connection.createStatement().execute("insert into T_BEAN (ID, NAME) values (0, 'Ben Hale');"); + connection.createStatement().execute( + "insert into T_ADDRESS (ID, BEAN_ID, VALUE) values (0, 0, 'Melbourne')"); + } catch (SQLException e) { + throw new RuntimeException("SQL exception occurred acquiring connection", e); + } finally { + if (connection != null) { + try { + connection.close(); + } catch (SQLException e) { + } + } + } + } + + private EntityManagerFactory getEntityManagerFactory(DataSource dataSource) throws Exception { + LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); + factory.setDataSource(dataSource); + factory.setPersistenceXmlLocation("classpath:org/springframework/webflow/persistence/persistence.xml"); + OpenJpaVendorAdapter openJpa = new OpenJpaVendorAdapter(); + factory.setJpaVendorAdapter(openJpa); + factory.afterPropertiesSet(); + return factory.getObject(); + } + + private void assertSessionNotBound() { + assertNull(TransactionSynchronizationManager.getResource(entityManagerFactory)); + } + + private void assertSessionBound() { + assertNotNull(TransactionSynchronizationManager.getResource(entityManagerFactory)); + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/persistence/flow-managed-persistence.xml b/spring-webflow/src/test/java/org/springframework/webflow/persistence/flow-managed-persistence.xml new file mode 100644 index 00000000..3b522ce6 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/persistence/flow-managed-persistence.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file