/*
* Copyright 2004-2014 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 java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.hibernate.FlushMode;
import org.hibernate.Interceptor;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.orm.hibernate4.SessionHolder;
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.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
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 native
* Hibernate API.
* <p>
* The general pattern is as follows:
* <ul>
* <li>When a flow execution starts, create a new Hibernate Session and bind it to flow scope under the name
* {@link #PERSISTENCE_CONTEXT_ATTRIBUTE}.
* <li>Before processing a flow execution request, expose the conversationally-bound session as the "current session"
* for the current thread.
* <li>When an existing flow pauses, unbind the session from the current thread.
* <li>When an existing flow ends, commit the changes made to the session 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 flow 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. In that case, the session's flush mode will be set to Manual and no intermediate changes will be
* flushed.
* <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 conversation, triggered by reaching a 'commit' end state.
*
* @author Keith Donald
* @author Juergen Hoeller
* @author Ben Hale
*/
public class HibernateFlowExecutionListener extends FlowExecutionListenerAdapter {
private static final boolean hibernate3Present = ClassUtils.isPresent("org.hibernate.connection.ConnectionProvider",
HibernateFlowExecutionListener.class.getClassLoader());
private static final Method openSessionMethod =
ReflectionUtils.findMethod(SessionFactory.class, "openSession");
private static final Method openSessionWithInterceptorMethod =
ReflectionUtils.findMethod(SessionFactory.class, "openSession", Interceptor.class);
private static final Method currentSessionMethod = ClassUtils.getMethod(SessionFactory.class, "getCurrentSession");
/**
* The name of the attribute the flow {@link Session persistence context} is indexed under.
*/
public static final String PERSISTENCE_CONTEXT_ATTRIBUTE = "persistenceContext";
private SessionFactory sessionFactory;
private TransactionTemplate transactionTemplate;
private Interceptor entityInterceptor;
/**
* Create a new Hibernate Flow Execution Listener using giving Hibernate session factory and transaction manager.
* @param sessionFactory the session factory to use
* @param transactionManager the transaction manager to drive transactions
*/
public HibernateFlowExecutionListener(SessionFactory sessionFactory, PlatformTransactionManager transactionManager) {
this.sessionFactory = sessionFactory;
this.transactionTemplate = new TransactionTemplate(transactionManager);
}
/**
* Sets the entity interceptor to attach to sessions opened by this listener.
* @param entityInterceptor the entity interceptor
*/
public void setEntityInterceptor(Interceptor entityInterceptor) {
this.entityInterceptor = entityInterceptor;
}
public void sessionStarting(RequestContext context, FlowSession session, MutableAttributeMap<?> input) {
boolean reusePersistenceContext = false;
if (isParentPersistenceContext(session)) {
if (isPersistenceContext(session.getDefinition())) {
setHibernateSession(session, getHibernateSession(session.getParent()));
reusePersistenceContext = true;
} else {
unbind(getHibernateSession(session.getParent()));
}
}
if (isPersistenceContext(session.getDefinition()) && (!reusePersistenceContext)) {
Session hibernateSession = createSession(context);
setHibernateSession(session, hibernateSession);
bind(hibernateSession);
}
}
public void paused(RequestContext context) {
if (isPersistenceContext(context.getActiveFlow())) {
Session session = getHibernateSession(context.getFlowExecutionContext().getActiveSession());
unbind(session);
session.disconnect();
}
}
public void resuming(RequestContext context) {
if (isPersistenceContext(context.getActiveFlow())) {
bind(getHibernateSession(context.getFlowExecutionContext().getActiveSession()));
}
}
public void sessionEnding(RequestContext context, FlowSession session, String outcome, MutableAttributeMap<?> output) {
if (isParentPersistenceContext(session)) {
return;
}
if (isPersistenceContext(session.getDefinition())) {
final Session hibernateSession = getHibernateSession(session);
Boolean commitStatus = session.getState().getAttributes().getBoolean("commit");
if (Boolean.TRUE.equals(commitStatus)) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus status) {
if (hibernate3Present) {
ReflectionUtils.invokeMethod(currentSessionMethod, sessionFactory);
}
else {
sessionFactory.getCurrentSession();
}
// nothing to do; a flush will happen on commit automatically as this is a read-write
// transaction
}
});
}
unbind(hibernateSession);
hibernateSession.close();
}
}
public void sessionEnded(RequestContext context, FlowSession session, String outcome, AttributeMap<?> output) {
if (isParentPersistenceContext(session)) {
if (!isPersistenceContext(session.getDefinition())) {
bind(getHibernateSession(session.getParent()));
}
}
}
public void exceptionThrown(RequestContext context, FlowExecutionException exception) {
if (context.getFlowExecutionContext().isActive()) {
if (isPersistenceContext(context.getActiveFlow())) {
unbind(getHibernateSession(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 Session createSession(RequestContext context) {
Session session;
if (entityInterceptor != null) {
if (hibernate3Present) {
try {
session = (Session) openSessionWithInterceptorMethod.invoke(sessionFactory, entityInterceptor);
} catch (IllegalAccessException ex) {
throw new IllegalStateException("Unable to open Hibernate 3 session", ex);
} catch (InvocationTargetException ex) {
throw new IllegalStateException("Unable to open Hibernate 3 session", ex);
}
} else {
session = sessionFactory.withOptions().interceptor(entityInterceptor).openSession();
}
} else {
if (hibernate3Present) {
try {
session = (Session) openSessionMethod.invoke(sessionFactory);
} catch (IllegalAccessException ex) {
throw new IllegalStateException("Unable to open Hibernate 3 session", ex);
} catch (InvocationTargetException ex) {
throw new IllegalStateException("Unable to open Hibernate 3 session", ex);
}
}
else {
session = sessionFactory.openSession();
}
}
session.setFlushMode(FlushMode.MANUAL);
return session;
}
private Session getHibernateSession(FlowSession session) {
return (Session) session.getScope().get(PERSISTENCE_CONTEXT_ATTRIBUTE);
}
private void setHibernateSession(FlowSession session, Session hibernateSession) {
session.getScope().put(PERSISTENCE_CONTEXT_ATTRIBUTE, hibernateSession);
}
private void bind(Session session) {
Object sessionHolder = (hibernate3Present ?
new org.springframework.orm.hibernate3.SessionHolder(session) : new SessionHolder(session));
TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder);
}
private void unbind(Session session) {
if (TransactionSynchronizationManager.hasResource(sessionFactory)) {
TransactionSynchronizationManager.unbindResource(sessionFactory);
}
}
}