Code:
import net.sf.hibernate.HibernateException;
import net.sf.hibernate.Session;
import net.sf.hibernate.SessionFactory;
import org.apache.log4j.Logger;
import org.springframework.jdbc.support.SQLExceptionTranslator;
import org.springframework.orm.hibernate.HibernateSystemException;
import org.springframework.orm.hibernate.SessionFactoryUtils;
import org.springframework.orm.hibernate.SessionHolder;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.Assert;
import org.springframework.webflow.*;
import java.io.Serializable;
/**
* Manages Hibernate Sessions using the SWF lifecycle. By default, each new flow session is paired
* with a new Hibernate Session which is bound to the executing thread using the
* <code>TransactionSynchronizationManager</code>. When a client request is made, this listener
* retrieves an existing Hibernate Session from the flow scope and reconnects it to the datasource.
* After the request is complete, the Hibernate Session is disconnected.
* <p/>
* By specifying the <code>subflowsParticipateInApplicationTransaction</code> annotation on the
* parent flow, all subflows may participate in the same Hibernate Session as the parent. If this is the case,
* the child flow defers closure of the Hibernate Session to the parent flow.
* <p/>
* Specifying the <code>clearOnFlowActivation</code> annotation on the flow causes the Hibernate
* Session to clear every time the flow becomes active. This is intended to be used to force Hibernate
* to reload objects from the db and roughly approximates a the OpenSessionView strategy. Be warned,
* however, that when this flag is set, activation of the flow will cause unpersisted changes to
* be lost.
*
* @author Alex Wolfe
*/
public class OpenSessionInFlowListener extends ExtendedFlowExecutionListener implements Serializable {
private static final transient Logger log = Logger.getLogger(OpenSessionInFlowListener.class);
private static final transient String HIBERNATE_SESSION_SHARED = "HIBERNATE_SESSION_SHARED";
private static final transient String SUBFLOWS_PARTICIPATE = "subflowsParticipateInApplicationTransaction";
private static final transient String CLEAR_ON_FLOW_ACTIVATION = "clearOnFlowActivation";
static final transient String HIBERNATE_SESSION = "HIBERNATE_SESSION";
private transient SQLExceptionTranslator sqlExceptionTranslator;
private transient SessionFactory sessionFactory;
private transient long nextSessionId = 1;
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
public SQLExceptionTranslator getSqlExceptionTranslator() {
if (sqlExceptionTranslator == null)
sqlExceptionTranslator = SessionFactoryUtils.newJdbcExceptionTranslator(sessionFactory);
return sqlExceptionTranslator;
}
/**
* The current flow is active. Reconnect the active flow's hibernate session. If the
* <code>clearOnFlowActivation</code> annotation is set on the flow, then invoke
* the <code>clear()</code> method on its hibernate session.
*
* @param ctx
*/
public void sessionActive(RequestContext ctx) {
Flow activeFlow = ctx.getFlowExecutionContext().getActiveFlow();
reconnectSession(activeFlow.getId(), getHibernateSession(ctx.getFlowScope()));
if (hasClearAnnotation(activeFlow))
getHibernateSession(ctx.getFlowScope()).getSession().clear();
}
/**
* Get the hibernate session from the parent flow scope.
*
* @param activeSession
* @return The parent's wrapped session
* @throws IllegalStateException if this flow expects to use the parent flow's hibernate session, but
* the parent does not have a hibernate session in scope.
*/
protected SessionWrapper getParentHibernateSession(FlowSession activeSession) {
FlowSession parentSession = activeSession.getParent();
Assert.notNull(parentSession,
"Flow attributes specify that the " + activeSession.getFlow().getId() +
" flow should reuse its parent's hibernate session, but no parent FlowSession exists"
);
SessionWrapper parentSessionWrapper = getHibernateSession(parentSession.getScope());
Assert.notNull(parentSessionWrapper,
new StringBuffer("Flow attributes specify that the ")
.append(activeSession.getFlow().getId())
.append(" flow should reuse its parent's hibernate session, but no parent hibernate session could be found in ")
.append(parentSession.getFlow().getId())
.append(" parent flow scope").toString()
);
return parentSessionWrapper;
}
/**
* Does the flow contain the annotation required to trigger hibernate session clear
* when a flow session is activated?
*
* @param flow The flow
* @return <code>true</code> if the <code>CLEAR_ON_FLOW_ACTIVATION</code> flag is specified in
* the flow, otherwise <code>false</code>
*/
protected boolean hasClearAnnotation(Flow flow) {
return flow.containsAttribute(CLEAR_ON_FLOW_ACTIVATION) &&
Boolean.valueOf((String) flow.getAttribute(CLEAR_ON_FLOW_ACTIVATION)).equals(Boolean.TRUE);
}
/**
* Does the flow contain the annotation required for child flows to participate in the same
* hibernate session?
*
* @param flow The flow
* @return <code>true</code> if the child flow can participate in the parent's hibernate session,
* otherwise, <code>false</code>
*/
protected boolean hasSubflowParticipationAnnotation(Flow flow) {
return flow.containsAttribute(SUBFLOWS_PARTICIPATE);
}
/**
* Does this flow use its parent's hibernate session?
*
* @param scope
* @return <code>true</code> if ths session is shared, otherwise <code>false</code>
*/
protected boolean canSubflowParticipate(Scope scope) {
Boolean canParticipate = (Boolean) scope.getAttribute(HIBERNATE_SESSION_SHARED);
return canParticipate != null && canParticipate.equals(Boolean.TRUE);
}
/**
* Set the flag that indicates whether the hibernate session for this flow is
* shared with the parent flow. This flag is set if the parent flow has the
* <code>SUBFLOWS_PARTICIPATE</code> annotation.
*
* @param scope
* @param flag
*/
protected void setSubflowParticipationFlag(String flowId, Scope scope, boolean flag) {
if (scope.containsAttribute(HIBERNATE_SESSION_SHARED)) return;
log.debug("Flow will " + (!flag ? "not " : "") + "participate in application transaction: " + flowId);
scope.setAttribute(HIBERNATE_SESSION_SHARED, Boolean.valueOf(flag));
}
/**
* Reconnect the hibernate session to the datasource.
*
* @param flowId The flow Id of the active flow
* @param wrapper The wrapped session
*/
protected void reconnectSession(String flowId, SessionWrapper wrapper) {
if (wrapper.getSession().isConnected()) return;
log.debug("Reconnecting session in " + flowId + " flow: " + wrapper);
try {
wrapper.getSession().reconnect();
bindSession(wrapper.getSession()); // bind the reconnected session to the thread
} catch (HibernateException e) {
throw new HibernateSystemException(e);
}
}
/**
* The executing flow's start state has been entered. Set the appropriate hibernate session in the flow
* scope. If the flow is participating in the same hibernate session as its parent, then set the parent's
* hibernate session in flow scope, otherwise use the thread bound session (they should be the same session).
* <p/>
* Each new session is wrapped by the {@link SessionWrapper} which maintains an identifier for the Session.
* This is helpful for determining session identity even through serialization.
*
* @param ctx The request context
* @param state The start state
*/
public void stateEntered(RequestContext ctx, State previousState, State state) {
SessionWrapper wrapper;
FlowSession activeSession = ctx.getFlowExecutionContext().getActiveSession();
String flowId = ctx.getFlowExecutionContext().getActiveFlow().getId();
setSubflowParticipationFlag(flowId, activeSession.getScope(), canSubflowParticipate(ctx.getRequestScope()));
if (canSubflowParticipate(ctx.getFlowScope())) {
wrapper = getParentHibernateSession(activeSession);
} else if (getHibernateSession(activeSession.getScope()) != null) {
wrapper = getHibernateSession(activeSession.getScope());
} else {
wrapper = new SessionWrapper(nextSessionId++, getBoundSession());
log.debug("Creating hibernate session " + wrapper.getId() + " for flow: " + flowId);
}
setHibernateSession(flowId, ctx.getFlowScope(), wrapper);
super.stateEntered(ctx, previousState, state);
}
/**
* A sub flow is launching. Reuses hibernate session from parent flow if that flow has the appropriate
* annotation. Otherwise, a new hibernate session is used for the subflow.
* <p/>
* In the event that a new hibernate session is created for the launching subflow, the parent's session
* is unbound from the thread and new session bound.
*
* @param parentSession The parent flow's session
* @param childFlow The child flow
* @param ctx The request context ]
* @throws IllegalStateException An invalid state is encountered while creating a new session for the subflow
* @see #hasSubflowParticipationAnnotation(Flow)
*/
public void launchingSubflow(FlowSession parentSession, Flow childFlow, RequestContext ctx) {
log.debug("Launching subflow: " + parentSession.getFlow().getId() + " -> " + childFlow.getId());
String parentFlowId = parentSession.getFlow().getId();
SessionWrapper parentWrapper = getHibernateSession(parentSession.getScope());
// For now, this flag is added to the request scope. It cannot be added to the
// child flow's scope yet because the child flow does not become active until later
// in the flow lifecycle. When the child flow is activated, this flag will be placed
// in the child flow's scope. This occurs in the stateEntered method.
setSubflowParticipationFlag(parentFlowId, ctx.getRequestScope(), hasSubflowParticipationAnnotation(parentSession.getFlow()));
if (canSubflowParticipate(ctx.getRequestScope())) {
log.debug("Reusing hibernate session " + parentWrapper + " from " + parentSession.getFlow().getId() + " flow in " + childFlow.getId() + " subflow");
} else {
SessionWrapper wrapper = getHibernateSession(parentSession.getScope());
disconnectSession(parentFlowId, wrapper.getSession(), new Long(wrapper.getId()));
Session parent = parentWrapper.getSession();
Session thread = getBoundSession();
Session child = replaceBoundSession();
log.debug("Replaced session " + parentWrapper + " with " + child + " on thread");
Assert.isTrue(parent == thread, "Parent scope has different hibernate session than thread");
Assert.isTrue(parent != child, "Session replacement returned the same hibernate session");
Assert.isTrue(child == getBoundSession(), "Session replacement did not replace the hibernate session");
Assert.isTrue(parent != getBoundSession(), "Session replacement returned the parent hibernate session");
}
}
/**
* A sub flow session has ended. If the flow had its own hibernate session it will be closed and the
* unbound from the thread.
*
* @param parent The parent flow session
* @param child The child flow session
* @param ctx The request context
* @throws IllegalStateException if the hibernate session should shared between parent and child, but
* the corresponding flow scopes contain different session instances or
* if the hibernate session should not be shared, but the parent and child
* flow scopes contain the same session instance
*/
public void subflowSessionEnded(FlowSession parent, FlowSession child, RequestContext ctx) {
log.info("Finished subflow: " + child.getFlow().getId() + " -> " + parent.getFlow().getId());
SessionWrapper childWrapper = getHibernateSession(child.getScope());
SessionWrapper parentWrapper = getHibernateSession(parent.getScope());
if (canSubflowParticipate(child.getScope())) {
if (log.isDebugEnabled())
log.debug("Deferring closure of hibernate session " + childWrapper.getId() + " from " + child.getFlow().getId() + " child flow " +
"to " + parent.getFlow().getId() + " parent flow");
Assert.isTrue(childWrapper.equals(parentWrapper),
new StringBuffer("Hibernate session for ")
.append(child.getFlow().getId())
.append(" child flow is not ")
.append("the same hibernate session as ")
.append(parent.getFlow().getId()).toString()
);
} else {
Assert.isTrue(!parentWrapper.equals(childWrapper), "Parent and child FlowSessions contain the same hibernate session");
log.debug("Closing session for flow " + child.getFlow().getId() + ": " + childWrapper);
closeSession();
log.debug("Replacing session " + childWrapper + " with session " + parentWrapper + " on thread");
replaceBoundSession(parentWrapper.getSession());
Assert.isTrue(getBoundSession() == parentWrapper.getSession(), "Parent hibernate session is not bound to thread");
}
}
/**
* The root flow session has ended. Close the flow's hibernate session and unbind it from the thread.
*
* @param endedSession The ended session
* @param ctx The request context
* @throws IllegalStateException if the hibernate session bound to the thread is not the same instance as
* the hibernate session found in the ended flow
*/
public void rootFlowSessionEnded(FlowSession endedSession, RequestContext ctx) {
log.debug("Finished flow: " + endedSession.getFlow().getId());
SessionWrapper wrapper = getHibernateSession(endedSession.getScope());
if (wrapper != null)
Assert.isTrue(wrapper.getSession() == getBoundSession(), "Unexpected hibernate session is bound to thread");
String flowId = endedSession.getFlow().getId();
log.debug("Closing session for flow " + flowId + ": " + getBoundSession());
closeSession();
}
/**
* A request from the client was processed. Disconnect the hibernate session from
* the datasource and unbind it from the thread.
*
* @param context The request context
*/
public void requestProcessed(RequestContext context) {
String flowId = null;
Long sessionId = null;
Session session = getBoundSession();
if (context.getFlowExecutionContext().isActive()) {
flowId = context.getFlowExecutionContext().getActiveFlow().getId();
SessionWrapper wrapper = getHibernateSession(context.getFlowScope());
session = wrapper.getSession();
sessionId = new Long(wrapper.getId());
}
disconnectSession(flowId, session, sessionId);
unbind();
}
protected void disconnectSession(String flowId, Session session, Long sessionId) {
Assert.notNull(session);
if (!session.isConnected()) return;
String sessionString = sessionId != null ? String.valueOf(sessionId.longValue()) : session.toString();
if (log.isDebugEnabled())
log.debug("Disconnecting session for flow" + (flowId != null ? " " + flowId : "") + ": " + sessionString);
try {
session.disconnect();
} catch (HibernateException e) {
throw new HibernateSystemException(e);
}
}
protected SessionWrapper getHibernateSession(Scope scope) {
return (SessionWrapper) scope.getAttribute(HIBERNATE_SESSION);
}
protected void setHibernateSession(String flowId, Scope scope, SessionWrapper wrapper) {
if (getHibernateSession(scope) != null &&
getHibernateSession(scope).equals(wrapper)) return;
log.debug("Hibernate session saved to scope for " + flowId + ": " + wrapper);
scope.setAttribute(HIBERNATE_SESSION, wrapper);
}
/**
* Get the Hibernate Session bound to the currently executing thread if it exists, otherwise return a
* new Session.
*
* @return Hibernate Session
*/
public Session getBoundSession() {
return bind(SessionFactoryUtils.getSession(sessionFactory, null, getSqlExceptionTranslator()));
}
/**
* Bind a Hibernate Session to the currently executing thread, replacing the existing Session if
* one is already bound.
*
* @param session
*/
public void bindSession(Session session) {
replaceBoundSession(session);
}
protected boolean threadHasSession() {
return TransactionSynchronizationManager.hasResource(sessionFactory);
}
/**
* Replace the Hibernate Session bound to the currently executing thread with
* a new session.
*
* @return The new session
*/
public Session replaceBoundSession() {
unbind();
return getBoundSession();
}
/**
* Replace this thread's session with the given session
*
* @param session The new session
*/
protected void replaceBoundSession(Session session) {
unbind();
bind(session);
}
protected void closeSession() {
Session session = getBoundSession();
unbind();
SessionFactoryUtils.releaseSession(session, sessionFactory);
Assert.isTrue(!session.isOpen(), "Hibernate Session is still open after release");
}
protected void unbind() {
if (threadHasSession())
TransactionSynchronizationManager.unbindResource(sessionFactory);
}
protected Session bind(Session session) {
if (!threadHasSession())
TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(session));
return session;
}
private final class SessionWrapper implements Serializable {
private final long id;
private final Session session;
public SessionWrapper(long id, Session session) {
this.session = session;
this.id = id;
}
public long getId() {
return id;
}
public Session getSession() {
return session;
}
public String toString() {
return String.valueOf(id);
}
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final SessionWrapper that = (SessionWrapper) o;
return id == that.id;
}
public int hashCode() {
return (int) (id ^ (id >>> 32));
}
}
}
Code:
import org.springframework.util.Assert;
import org.springframework.webflow.*;
import org.springframework.webflow.execution.EnterStateVetoException;
import org.springframework.webflow.execution.FlowExecutionListenerAdapter;
import java.util.Map;
/**
* Adaptation of flow lifecycle events.
*
* @author Alex Wolfe
*/
public class ExtendedFlowExecutionListener extends FlowExecutionListenerAdapter {
private static final String FIRST_STATE_ENTERED = "FIRST_STATE_ENTERED";
/**
* Invoked when a flow is launched. The launching flow is not active.
*
* @param newFlow The launching flow
* @param ctx The request context
*/
public void launchingNewFlow(Flow newFlow, RequestContext ctx) {
}
/**
* Invoked when a subflow is launched. The child flow session is not
* available to implementations of this method because the flow session for
* the child flow has not yet started.
* <p/>
* If you need to add items to the subflow scope, use the
* {@link #sessionActive(RequestContext)} method.
*
* @param parentSession The active parent flow session
* @param child The child flow
* @param ctx The request context
*/
public void launchingSubflow(FlowSession parentSession, Flow child, RequestContext ctx) {
}
/**
* Invoked when the a sub-flow session has ended.
*
* @param parent The parent flow's session
* @param child The ended child flow's session
* @param ctx The request context
*/
public void subflowSessionEnded(FlowSession parent, FlowSession child, RequestContext ctx) {
}
/**
* Invoked when the root flow session has ended.
*
* @param endedSession The ended session
* @param ctx The request context
*/
public void rootFlowSessionEnded(FlowSession endedSession, RequestContext ctx) {
}
/**
* The currently executing flow session is active.
* This occurs after the first state for the executing request has been entered.
* Invoked once per request and provides access to the active flow session prior to
* any actions being performed for the current request.
*
* @param ctx The request context
*/
public void sessionActive(RequestContext ctx) {
}
/**
* Called when any client request is submitted to manipulate this flow execution.
* Sets a flag in the request scope that is activated when the first state is entered
* during this request. This flag is required in order to trigger execution of the
* {@link #sessionActive(RequestContext)} method.
*
* @param context The request context
*/
public void requestSubmitted(RequestContext context) {
context.getRequestScope().setAttribute(FIRST_STATE_ENTERED, Boolean.FALSE);
}
/**
* The flow session is starting. This method invokes {@link #launchingNewFlow(Flow, RequestContext)}
* if the launching flow session is the root flow. Otherwise, the
* {@link #launchingSubflow(FlowSession, Flow, RequestContext)} method is invoked.
*
* @param context The request context
* @param startState The start state
* @param input
* @throws EnterStateVetoException The start state transition was not allowed
*/
public void sessionStarting(RequestContext context, State startState, Map input) throws EnterStateVetoException {
FlowExecutionContext exeCtx = context.getFlowExecutionContext();
FlowSession activeSession = exeCtx.isActive() ? exeCtx.getActiveSession() : null;
Flow newFlow = startState.getFlow();
if (activeSession == null && newFlow != null)
launchingNewFlow(newFlow, context);
else if (activeSession != null & newFlow != null)
launchingSubflow(activeSession, newFlow, context);
}
/**
* Called when a flow execution session ends.
* If the ended session was the root session of the flow execution,
* the {@link #rootFlowSessionEnded(FlowSession, RequestContext)} method is invoked
* and the <code>SESSION_ACTIVE</code> flag is reset.
* <p/>
* If the ended session was not the root session, then the
* {@link #subflowSessionEnded(FlowSession, FlowSession, RequestContext)}
* method is invoked.
*
* @param context The source of the event
* @param endedSession The ended FlowSession
*/
public void sessionEnded(RequestContext context, FlowSession endedSession) {
FlowExecutionContext exeCtx = context.getFlowExecutionContext();
FlowSession newSession = (exeCtx.isActive() ? exeCtx.getActiveSession() : null);
if (endedSession != null && newSession != null) {
subflowSessionEnded(newSession, endedSession, context);
context.getRequestScope().setAttribute(FIRST_STATE_ENTERED, Boolean.FALSE);
} else if (endedSession != null && newSession == null)
rootFlowSessionEnded(endedSession, context);
}
/**
* Called when a state transitions, after the transition occured. If the state entered
* is the first state entered for the request, then the {@link #sessionActive(RequestContext)} method is invoked.
*
* @param context The request context
* @param previousState The previous state
* @param state The entered state
*/
public void stateEntered(RequestContext context, State previousState, State state) {
if (!firstStateEntered(context)) {
context.getRequestScope().setAttribute(FIRST_STATE_ENTERED, Boolean.TRUE);
sessionActive(context);
}
}
/**
* Determine whether the current request has entered a state.
*
* @param ctx The request context
* @return <code>true</code> if a state has already been entered during the current request,
* otherwise <code>false</code>
*/
protected boolean firstStateEntered(RequestContext ctx) {
Assert.isTrue(ctx.getRequestScope().getAttribute(FIRST_STATE_ENTERED) != null);
return ctx.getRequestScope().getAttribute(FIRST_STATE_ENTERED).equals(Boolean.TRUE);
}
protected boolean isStartState(RequestContext ctx, State state) {
return state.equals(ctx.getFlowExecutionContext().getActiveFlow().getStartState());
}
}