diff --git a/spring-webflow-sandbox/build.xml b/spring-webflow-sandbox/build.xml index 3973c36d..f8d80e0b 100644 --- a/spring-webflow-sandbox/build.xml +++ b/spring-webflow-sandbox/build.xml @@ -8,6 +8,8 @@ + + diff --git a/spring-webflow-sandbox/ivy.xml b/spring-webflow-sandbox/ivy.xml index 3803246d..822fb458 100644 --- a/spring-webflow-sandbox/ivy.xml +++ b/spring-webflow-sandbox/ivy.xml @@ -15,7 +15,18 @@ + + + + + + + + + + + diff --git a/spring-webflow-sandbox/project.properties b/spring-webflow-sandbox/project.properties index f80ab3a7..55ed07f5 100644 --- a/spring-webflow-sandbox/project.properties +++ b/spring-webflow-sandbox/project.properties @@ -1,7 +1,7 @@ # properties defined in this file are overridable by a local build.properties in this project dir # The location of the common build system -common.build.dir=${basedir}/../../common-build +common.build.dir=${basedir}/../common-build javac.source=1.5 javac.target=1.5 diff --git a/spring-webflow-sandbox/src/etc/test-resources/log4j.properties b/spring-webflow-sandbox/src/etc/test-resources/log4j.properties index 0b8127b5..b10ab1a5 100644 --- a/spring-webflow-sandbox/src/etc/test-resources/log4j.properties +++ b/spring-webflow-sandbox/src/etc/test-resources/log4j.properties @@ -16,4 +16,3 @@ log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - <%m>%n log4j.category.org.springframework.webflow=DEBUG log4j.category.org.springframework.binding=DEBUG -#Just for test \ No newline at end of file diff --git a/spring-webflow-sandbox/src/main/java/org/springframework/util/AbstractReadOnlyResourceHolder.java b/spring-webflow-sandbox/src/main/java/org/springframework/util/AbstractReadOnlyResourceHolder.java new file mode 100644 index 00000000..5823badb --- /dev/null +++ b/spring-webflow-sandbox/src/main/java/org/springframework/util/AbstractReadOnlyResourceHolder.java @@ -0,0 +1,12 @@ +package org.springframework.util; + +/** + * Read-only resource holder. + * + * @author Maxim Petrashev + */ +public abstract class AbstractReadOnlyResourceHolder extends AbstractResourceHolder { + public final void set(E aObject) { + throw new UnsupportedOperationException(); + } +} diff --git a/spring-webflow-sandbox/src/main/java/org/springframework/util/AbstractResourceHolder.java b/spring-webflow-sandbox/src/main/java/org/springframework/util/AbstractResourceHolder.java new file mode 100644 index 00000000..5fce9a33 --- /dev/null +++ b/spring-webflow-sandbox/src/main/java/org/springframework/util/AbstractResourceHolder.java @@ -0,0 +1,23 @@ +package org.springframework.util; + +import org.springframework.beans.factory.support.MethodReplacer; + +import java.lang.reflect.Method; + +/** + * Base abstract implementation of resource holder that implement method replacment logic. + * + * @author Maxim Petrashev + */ +public abstract class AbstractResourceHolder implements ResourceHolder + , MethodReplacer {//todo replace on injector in config + public Object reimplement(Object aObj, Method aMethod, Object[] aArgs) throws Throwable { + Object retVal = null; + if( aArgs.length == 0 ){ + retVal = get(); + } else { + set( (E) aArgs[0] ); + } + return retVal; + } +} diff --git a/spring-webflow-sandbox/src/main/java/org/springframework/util/ResourceHolder.java b/spring-webflow-sandbox/src/main/java/org/springframework/util/ResourceHolder.java new file mode 100644 index 00000000..1e9ab9b2 --- /dev/null +++ b/spring-webflow-sandbox/src/main/java/org/springframework/util/ResourceHolder.java @@ -0,0 +1,11 @@ +package org.springframework.util; + +/** + * Base interface for object holder concept. + * + * @author Maxim Petrashev + */ +public interface ResourceHolder { + E get(); + void set(E aObject); +} diff --git a/spring-webflow-sandbox/src/main/java/org/springframework/webflow/execution/ConversationLifecycleListener.java b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/execution/ConversationLifecycleListener.java new file mode 100644 index 00000000..a5b822ba --- /dev/null +++ b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/execution/ConversationLifecycleListener.java @@ -0,0 +1,96 @@ +package org.springframework.webflow.execution; + +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.core.collection.MutableAttributeMap; + +/** + * Listener interface for callback notification about phases in during conversation + * + * @author Maxim Petrashev + */ +public interface ConversationLifecycleListener { + /** + * Invoked when a flow is launched. The launching flow is not active. + * + * @param aNewFlow The launching flow + * @param aContext The request context + */ + + void startingFlow(FlowDefinition aNewFlow, RequestContext aContext); + + void flowStarted(RequestContext aContext, MutableAttributeMap aInput); + /** + * Invoked when the root flow session has ended. + * + * @param aEndedSession The ended session + * @param aContext The request context + */ + void flowEnded(FlowSession aEndedSession, RequestContext aContext); + + void endingFlow(RequestContext aContext, MutableAttributeMap aOutput); + /** + * Invoked in parent flow before spawning in subflow but after input mapping has been happened. + * The child flow session is not available to implementations of this method because the flow session for + * the child flow has not yet started.

If you need to add items to the + * subflow scope, put them in aInput. + * + * @param aParentSession The active parent flow session + * @param aChild The child flow + * @param aContext The request context + * @param aInput The input map + */ + void startingSubflow(FlowSession aParentSession, FlowDefinition aChild, RequestContext aContext + , MutableAttributeMap aInput); + /** + * 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.

If you need to add items to the + * subflow scope, put them in aInput. + * + * @param aContext The request context + * @param aInput The input map + */ + void subflowStarted(RequestContext aContext , MutableAttributeMap aInput); + /** + * Invoked in subflow flow before spawning back to parent flot but after output mapping has been happened. + * The parent flow session is not available to implementations of this method because the flow session for + * the parent flow has not yet continued.

If you need to add items to the + * parent flow scope, put them in aOutput. + * + * @param aContext The request context + * @param aOutput The input map + */ + void endingSubflow(RequestContext aContext , MutableAttributeMap aOutput); + /** + * Invoked when a parent flow is launched back. The child flow session is not + * available to implementations of this method because the flow session for + * the child flow has not yet started.

If you need to add items to the + * subflow scope, put them in aInput. + * + * @param aParentSession The active parent flow session + * @param aChild The child flow + * @param aContext The request context + * @param aOutput The input map + */ + void subflowEnded(FlowSession aParentSession, FlowDefinition aChild, RequestContext aContext + , MutableAttributeMap aOutput); + /** + * The currently executing flow session is active. This occurs after the + * first event for the executing request has been signaled or resume event has been happended. Invoked once per + * request and provides access to the active flow session prior to any + * actions being performed. + * + * @param aContext The request context + */ + void sessionActive(RequestContext aContext); + /** + * The currently executing flow session is active. This occurs after the + * first event for the executing request has been signaled or resume event has been happended. Invoked once per + * request and provides access to the active flow session prior to any + * actions being performed. + * + * @param aContext The request context + */ + void sessionDeactive(RequestContext aContext); + +} diff --git a/spring-webflow-sandbox/src/main/java/org/springframework/webflow/execution/ConversationLifecycleListenerAdapter.java b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/execution/ConversationLifecycleListenerAdapter.java new file mode 100644 index 00000000..0dbc5137 --- /dev/null +++ b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/execution/ConversationLifecycleListenerAdapter.java @@ -0,0 +1,44 @@ +package org.springframework.webflow.execution; + +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.core.collection.MutableAttributeMap; + +/** + * + * @author Maxim Petrashev + */ +public abstract class ConversationLifecycleListenerAdapter implements ConversationLifecycleListener { + + public void startingFlow(FlowDefinition aNewFlow, RequestContext aContext) { + } + + public void flowStarted(RequestContext aContext, MutableAttributeMap aInput) { + } + + public void flowEnded(FlowSession aEndedSession, RequestContext aContext) { + } + + public void endingFlow(RequestContext aContext, MutableAttributeMap aOutput) { + } + + public void startingSubflow(FlowSession aParentSession, FlowDefinition aChild, RequestContext aContext, MutableAttributeMap aInput) { + } + + public void subflowStarted(RequestContext aContext, MutableAttributeMap aInput) { + } + + public void subflowStarted(FlowSession aParentSession, FlowDefinition aChild, RequestContext aContext, MutableAttributeMap aInput) { + } + + public void endingSubflow(RequestContext aContext, MutableAttributeMap aOutput) { + } + + public void subflowEnded(FlowSession aParentSession, FlowDefinition aChild, RequestContext aContext, MutableAttributeMap aOutput) { + } + + public void sessionActive(RequestContext aContext) { + } + + public void sessionDeactive(RequestContext aContext) { + } +} diff --git a/spring-webflow-sandbox/src/main/java/org/springframework/webflow/execution/ExtendedFlowExecutionListenerInterceptor.java b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/execution/ExtendedFlowExecutionListenerInterceptor.java new file mode 100644 index 00000000..01f022f6 --- /dev/null +++ b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/execution/ExtendedFlowExecutionListenerInterceptor.java @@ -0,0 +1,181 @@ +package org.springframework.webflow.execution; + +import org.springframework.webflow.engine.SubflowState; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.definition.StateDefinition; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A listener/interceptor whose purpose is to adapt a HandlerInterceptor and + * FlowExecutionListener to provide expanded coverage at the beginning and + * ending of a request coming into a flow controller. The idea is to signal when + * a flow starts/activates, then signal when it has deactivated or ended. In + * addition, it should be signaled when a subflow starts/activates and + * ends/deactivates. + *

+ * Adapted from Alex Wolfe's post at + * http://forum.springframework.org/showthread.php?t=17633 + * + * + * @author Andrew Ebaugh + * @author Maxim Petrashev + */ +public class ExtendedFlowExecutionListenerInterceptor extends FlowExecutionListenerAdapter { + protected final Log _logger = LogFactory.getLog(getClass()); + + private static String FIRST_EVENT_SIGNALED = ExtendedFlowExecutionListenerInterceptor.class.getName() + ".FIRST_EVENT_SIGNALED"; + private static String CURRENT_SESSION_ENDED = ExtendedFlowExecutionListenerInterceptor.class.getName() + ".CURRENT_SESSION_ENDED"; + + + public ExtendedFlowExecutionListenerInterceptor(ConversationLifecycleListener aConversationLifecycleListener) { + _conversationLifecycleListener = aConversationLifecycleListener; + } + + private ConversationLifecycleListener _conversationLifecycleListener; + + /** + * 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 ConversationLifecycleListener#sessionActive(RequestContext)} + * method. + * + * @param aContext The request aContext + */ + public final void requestSubmitted(RequestContext aContext) { + aContext.getRequestScope().put(FIRST_EVENT_SIGNALED, Boolean.FALSE); + aContext.getRequestScope().put(CURRENT_SESSION_ENDED, Boolean.FALSE); + } + + public final void eventSignaled(RequestContext aContext, Event aEvent) { + signalAction(aContext); + } + + public final void stateEntered(RequestContext aContext, StateDefinition aPreviousState, StateDefinition aState) { + signalAction(aContext); + } + + /** + * Called when an event is signaled, or a state is entered, but before any + * potential transition or actions occurs. If the action signaled is the + * first for the request, then the {@link ConversationLifecycleListener#sessionActive(RequestContext)} + * method is invoked. It is necessary that this be called prior to any state + * transitions to provide listeners coverage of any state + * exit/transition/entry actions. + * + * @param aContext The request aContext + */ + private void signalAction(RequestContext aContext) { + if (!firstEventSignaled(aContext)) { + aContext.getRequestScope().put(FIRST_EVENT_SIGNALED, Boolean.TRUE); + _conversationLifecycleListener.sessionActive(aContext); + } + } + + /** + * A aFlow session is starting. This method invokes + * {@link ConversationLifecycleListener#startingFlow(FlowDefinition, RequestContext)} if the launching aFlow + * session is the root aFlow. Otherwise, the + * {@link ConversationLifecycleListener#startingSubflow(FlowSession, FlowDefinition, RequestContext,MutableAttributeMap)} method is + * invoked. + * + * @param aContext The request aContext + * @param aInput + * @throws EnterStateVetoException + * The start state transition was not allowed + * @param aFlowDefinition + */ + public final void sessionStarting(RequestContext aContext, FlowDefinition aFlowDefinition, MutableAttributeMap aInput) { + aContext.getRequestScope().put(FIRST_EVENT_SIGNALED, Boolean.TRUE); + aContext.getRequestScope().put(CURRENT_SESSION_ENDED, Boolean.FALSE); + FlowExecutionContext executionContext = aContext.getFlowExecutionContext(); + FlowSession activeSession = executionContext.isActive() ? executionContext .getActiveSession() : null; + // when starting in default subflow start state? + if (activeSession != null + && SubflowState.class.isInstance(activeSession.getState())) { + aFlowDefinition = ((SubflowState) activeSession.getState()).getSubflow(); + } + + if (activeSession == null) { + _conversationLifecycleListener.startingFlow(aFlowDefinition, aContext); + } else { + _conversationLifecycleListener.startingSubflow(activeSession, aFlowDefinition, aContext, null); + } + } + + public void sessionStarted(RequestContext aContext, FlowSession aSession) { + if( aSession.isRoot() ) { + _conversationLifecycleListener.flowStarted(aContext, null ); + } else { + _conversationLifecycleListener.subflowStarted(aContext,null); + } + } + + public void sessionEnding(RequestContext aContext, FlowSession aSession, MutableAttributeMap aOutput) { + if( aSession.isRoot() ) { + _conversationLifecycleListener.endingFlow(aContext, null ); + } else { + _conversationLifecycleListener.endingSubflow(aContext,null); + } + } + + public void requestProcessed(RequestContext aContext) { + FlowExecutionContext executionContext = aContext.getFlowExecutionContext(); + if( executionContext.isActive() ) { + _conversationLifecycleListener.sessionDeactive(aContext ); + } + } + + /** + * Called when a flow execution session ends. If the ended session was the + * root session of the flow execution, the + * {@link ConversationLifecycleListener#flowEnded(FlowSession, RequestContext)} method is + * invoked.

If the ended session was not the root session, then the + * {@link ConversationLifecycleListener#subflowEnded(FlowSession, FlowDefinition, RequestContext, MutableAttributeMap)} } + * method is invoked. Prior to this, and in either case, the + * {@link ConversationLifecycleListener#sessionDeactive(RequestContext)} method is invoked. + * + * @param aContext The source of the event + * @param aEndedSession The ended FlowSession + */ + public final void sessionEnded(RequestContext aContext, FlowSession aEndedSession, AttributeMap aSessionOutput) { + FlowExecutionContext exeCtx = aContext.getFlowExecutionContext(); + FlowSession newSession = exeCtx.isActive() ? exeCtx.getActiveSession() : null; + if (aEndedSession != null) { + try{ + if (aEndedSession.isRoot()) { + _conversationLifecycleListener.flowEnded(aEndedSession, aContext); + } else { + _conversationLifecycleListener.subflowEnded(newSession, aEndedSession.getDefinition(), aContext, null); + } + } catch( RuntimeException e ){ //todo to think which base exception need wrap + String message = "Can't end session"; + if( _logger.isWarnEnabled() ) { + _logger.warn(message,e); + } + throw new FlowExecutionException( exeCtx.getDefinition().getId() + , newSession != null ? newSession.getState().getId() : aEndedSession.getState().getId() + , message, e); + } + } + aContext.getRequestScope().put(CURRENT_SESSION_ENDED, Boolean.TRUE); + } + + /** + * Determine whether the current request has handled an event. + * + * @param aContext The request context + * @return true if an event has already been signaled during + * the current request, otherwise false + */ + private boolean firstEventSignaled(RequestContext aContext) { + return aContext.getRequestScope().get(FIRST_EVENT_SIGNALED).equals(Boolean.TRUE); + } + public void resumed(RequestContext context) { + signalAction(context); + } +} diff --git a/spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/DefaultEntityManagerLifecycleController.java b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/DefaultEntityManagerLifecycleController.java new file mode 100644 index 00000000..0d46b356 --- /dev/null +++ b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/DefaultEntityManagerLifecycleController.java @@ -0,0 +1,71 @@ +package org.springframework.webflow.jpa; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.util.Assert; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.orm.jpa.EntityManagerHolder; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; + +/** + * Default abstract JPA implementation for {@link EntityManagerLifecycleController}. + * + * @author Maxim Petrashev + */ +public abstract class DefaultEntityManagerLifecycleController implements EntityManagerLifecycleController { + private EntityManagerFactory _entityManagerFactory; + + public DefaultEntityManagerLifecycleController(EntityManagerFactory aEntityManagerFactory) { + _entityManagerFactory = aEntityManagerFactory; + } + + public void close(EntityManager aEntityManager) { + Assert.isTrue(aEntityManager.isOpen(), "Entity manager was already closed"); + aEntityManager.close(); + } + + public void deactivate(EntityManager aEntityManager) { + unbind(aEntityManager); + if (_log.isDebugEnabled()) { + _log.debug("Entity manager unbinded: " + aEntityManager.isOpen()); + } + } + + public EntityManager create() { + return _entityManagerFactory.createEntityManager(); + } + + public void flush(EntityManager aEntityManager) { + try { + aEntityManager.flush(); + } catch (RuntimeException e) {//todo review this code + aEntityManager.getTransaction().rollback(); + throw e; + } + } + + public void activate(EntityManager aEntityManager) { + bind(aEntityManager); + if (_log.isDebugEnabled()) { + _log.debug("Session activate: " + aEntityManager.isOpen()); + } + } + protected void bind(EntityManager aEntityManager) { + TransactionSynchronizationManager.bindResource(_entityManagerFactory, new EntityManagerHolder(aEntityManager)); + } + + protected void unbind( EntityManager aEntityManager ) { + synchronized(_entityManagerFactory){//todo is this need? + Assert.isTrue( TransactionSynchronizationManager.hasResource(_entityManagerFactory) ); //todo remove this code. Resource must be already present. + TransactionSynchronizationManager.unbindResource(_entityManagerFactory); + } + } + + protected EntityManagerFactory getEntityManagerFactory() { + return _entityManagerFactory; + } + + protected final Log _log = LogFactory.getLog(getClass()); +} diff --git a/spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/EntityManagerLifecycleController.java b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/EntityManagerLifecycleController.java new file mode 100644 index 00000000..a5ac5be4 --- /dev/null +++ b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/EntityManagerLifecycleController.java @@ -0,0 +1,43 @@ +package org.springframework.webflow.jpa; + +import javax.persistence.EntityManager; + +/** + * Lifycycle controller that hide vendor specific routines for entity manager like: + *

    + *
  • Application transaction commit implementation. See, for example, + * {@link org.hibernate.annotations.FlushModeType.MANUAL}
  • + *
  • Binding/Unbinding persistence context resources for current thread
  • + *
+ * + * @author Maxim Petrashev + */ +public interface EntityManagerLifecycleController { + /** + * Create new entity manager and return wrapper for it with aId id. + */ + EntityManager create(); + + /** + * Reconnect entity manager and bind to current thread. + * @param aEntityManager + */ + void activate(EntityManager aEntityManager); + /** + * Disconnect current session and unbind from current thread. + * @param aEntityManager + */ + void deactivate(EntityManager aEntityManager); + + /** + * Commit application transaction. + * @param aEntityManager + */ + void flush(EntityManager aEntityManager); + /** + * Close opened entity manager. + * @param aEntityManager + */ + void close(EntityManager aEntityManager); + +} diff --git a/spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/OpenEntityManagerPerConversationFlowListener.java b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/OpenEntityManagerPerConversationFlowListener.java new file mode 100644 index 00000000..c9ff306b --- /dev/null +++ b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/OpenEntityManagerPerConversationFlowListener.java @@ -0,0 +1,143 @@ +package org.springframework.webflow.jpa; + +import org.springframework.webflow.execution.ExtendedFlowExecutionListenerInterceptor; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.FlowSession; +import org.springframework.webflow.execution.FlowExecutionContext; +import org.springframework.webflow.execution.ConversationLifecycleListener; +import org.springframework.webflow.execution.ConversationLifecycleListenerAdapter; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.definition.StateDefinition; +import org.springframework.webflow.engine.EndState; +import org.springframework.web.context.request.WebRequestInterceptor; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import javax.persistence.EntityManager; +/** + * Intended for those wating a long session model within webflows. A entity + * manager is created when the flow begins, and is disconnected and reconnected + * as necessary throughout the lifecycle of the flow. In particular, it should + * be thought of as both a + * {@link org.springframework.webflow.execution.FlowExecutionListener} and + * {@link WebRequestInterceptor} + * that binds a + * entity manager to the current thread for the activate span of a FlowSession. + * This implementation shares the entity manager between a parent flow and any + * subflows. + *

+ * To facilitate the long session idea, objects within the flow will be + * reassociated with the jpa persistence context when the flow is + * re-activated. Deserialized jpa flow scope + * objects lose their association to the persistence context, and thus result in + * problems when you attempt to perform persistence operations (run into + * non-unique object exceptions, null sessions in persistent collections, and a + * host of other weird behaviors). There is a basic facility to apply a + * flushMode to new entity managers that are created, but not as + * sophisticated as that provided by a HibernateAccessor. Config example: + * + *
+ *      	<bean id="openEntityManagerFlowListener"
+ *       			class="org.springframework.webflow.jpa.OpenEntityManagerPerConversationFlowListener">
+ *       		<constructor ref="_lifecycleController"/>
+ *      	</bean>
+ * 
+ * + *

+ * Adapted from Alex Wolfe's post at + * http://forum.springframework.org/showthread.php?t=17633 + * + * @author Maxim Petrashev + */ +public class OpenEntityManagerPerConversationFlowListener extends ConversationLifecycleListenerAdapter { + protected final Log _logger = LogFactory.getLog( getClass() ); + + private final EntityManagerLifecycleController _lifecycleController; + + /** + * Attribute name for annotated state which mark end-state and application commit end-state. + */ + public static final String APPLICATION_TRANSACTION_COMMIT_ATTR_NAME = "applicationTransactionCommit"; + /** + * Attribute name under wich in conversation scope will be stored entity manager for conversation. + */ + private static final String ENTITY_MANAGER_ATTR_NAME = OpenEntityManagerPerConversationFlowListener.class.getName() + ".ENTITY_MANAGER"; + + protected OpenEntityManagerPerConversationFlowListener(EntityManagerLifecycleController aLifecycleController ) { + _lifecycleController = aLifecycleController; + } + + protected EntityManager getEntityManager(RequestContext aContext){ + return (EntityManager) aContext.getConversationScope().get( ENTITY_MANAGER_ATTR_NAME ); + } + + /** + * Create entity manager for new conversation. + * @param aNewFlow + * @param aContext + */ + public void startingFlow(FlowDefinition aNewFlow, RequestContext aContext) { + _logger.debug("Creating entity manager for flow: " + aNewFlow.getId()); + EntityManager entityManager = _lifecycleController.create(); + _lifecycleController.activate( entityManager ); + aContext.getConversationScope().put(ENTITY_MANAGER_ATTR_NAME, entityManager); + } + + /** + * Try commit application transaction on application transaction commit end state. + * Clean also all resources that were allocated for conversation entity manager. + * @param aEndedSession + * @param aContext + */ + public void flowEnded(FlowSession aEndedSession, RequestContext aContext) { + EntityManager entityManager = getEntityManager(aContext); + try{ + if (isApplicationTransactionCommitState(aEndedSession.getState())) { + _lifecycleController.flush(entityManager); + } + }finally{ + try{ + _lifecycleController.deactivate(entityManager); + }finally{ + _lifecycleController.close( entityManager ); + } + } + } + + /** + * {@inheritDoc} + */ + public void sessionActive(RequestContext aContext) { + EntityManager entityManager = getEntityManager(aContext); + _lifecycleController.activate( entityManager ); + } + + /** + * Deactivate current entity manager on end of request handling process. + */ + public void sessionDeactive(RequestContext aContext) { + FlowExecutionContext flowExecutionContext = aContext.getFlowExecutionContext(); + if( flowExecutionContext.isActive() ) {//todo need CommandManager or request specific lifecycleController + EntityManager entityManager = getEntityManager(aContext); + _lifecycleController.deactivate( entityManager ); + } else { + //entity manager already was closed in flowEnded method + } + } + + /** + * Return is aState application commit state or not. Returns true if aState is EndState and aState is annotated + * by {@link #APPLICATION_TRANSACTION_COMMIT_ATTR_NAME} attribute. + */ + protected boolean isApplicationTransactionCommitState(StateDefinition aState) { + boolean retVal = false; + if (aState instanceof EndState) { + retVal = aState.getAttributes().get(APPLICATION_TRANSACTION_COMMIT_ATTR_NAME, "false").equals("true"); + } + return retVal; + } + + public static final String CURRENT_ENTITY_MANAGER_KEY_ATTR_NAME = OpenEntityManagerPerConversationFlowListener.class.getName() + ".CURRENT_ENTITY_MANAGER_KEY"; + //todo review exceptionThrown +} diff --git a/spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/TransactionSynchronizationManagerEnityManagerHolder.java b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/TransactionSynchronizationManagerEnityManagerHolder.java new file mode 100644 index 00000000..e9a2d7c8 --- /dev/null +++ b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/TransactionSynchronizationManagerEnityManagerHolder.java @@ -0,0 +1,25 @@ +package org.springframework.webflow.jpa; + +import org.springframework.orm.jpa.EntityManagerHolder; +import org.springframework.util.AbstractReadOnlyResourceHolder; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import javax.persistence.EntityManager; + +/** + * Resource holder that is wrapper for TransactionSynchronizationManager. + * + * @author Maxim Petrashev + */ +public class TransactionSynchronizationManagerEnityManagerHolder extends AbstractReadOnlyResourceHolder { + public EntityManager get() { + EntityManagerHolder emHolder = (EntityManagerHolder) TransactionSynchronizationManager.getResource( _key ); + return emHolder.getEntityManager(); + } + + public TransactionSynchronizationManagerEnityManagerHolder(Object aKey) { + _key = aKey; + } + + private Object _key; +} diff --git a/spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/hibernate/EntityManagerLifecycleController.java b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/hibernate/EntityManagerLifecycleController.java new file mode 100644 index 00000000..65c44c2c --- /dev/null +++ b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/hibernate/EntityManagerLifecycleController.java @@ -0,0 +1,68 @@ +package org.springframework.webflow.jpa.hibernate; + +import org.springframework.webflow.jpa.DefaultEntityManagerLifecycleController; +import org.hibernate.FlushMode; +import org.hibernate.SessionFactory; +import org.hibernate.classic.Session; +import org.hibernate.context.ManagedSessionContext; + +import javax.persistence.EntityManagerFactory; +import javax.persistence.EntityManager; +import javax.persistence.EntityTransaction; + +/** + * Hibernate specific implementation of EntityManagerLifecycleController interface. Set for each new entity manager + * hibernate specific flush mode and starts new JPA transaction on activation, and commit it on deactivation + * + * @author Maxim Petrashev + */ +public class EntityManagerLifecycleController extends DefaultEntityManagerLifecycleController { + + public EntityManagerLifecycleController(EntityManagerFactory aEntityManagerFactory) { + super(aEntityManagerFactory); + } + public EntityManager create() { + EntityManager retVal = super.create(); + Session session = HibernateUtils.getSession(retVal); + session.setFlushMode(FlushMode.MANUAL); //todo review this code + return retVal; + } + protected void unbind( EntityManager aEntityManager ) { + try{ + SessionFactory sessionFactory = HibernateUtils.getSessionFactory( getEntityManagerFactory() ); + ManagedSessionContext.unbind(sessionFactory); + }finally{ + super.unbind( aEntityManager ); + } + } + + protected void bind(EntityManager aEntityManager) { + super.bind(aEntityManager); + //todo remove this code in hibernate specific class + ManagedSessionContext.bind( HibernateUtils.getSession( aEntityManager ) ); + } + + public void activate(EntityManager aEntityManager) { + beginTransaction(aEntityManager); + super.activate(aEntityManager); + } + + public void deactivate(EntityManager aEntityManager) { + try{ + disconnectSession(aEntityManager); + } finally { + super.deactivate(aEntityManager); + } + } + + protected void beginTransaction(EntityManager aEntityManager) {//todo is it method need? May be transaction aspect has to cover it? + //begin database transaction for taking available connection + aEntityManager.getTransaction().begin(); + } + protected void disconnectSession(EntityManager aEntityManager) {//todo is it method need? May be transaction aspect has to cover it? + EntityTransaction transaction = aEntityManager.getTransaction(); + transaction.commit(); + //todo is it need ? aSession.disconnect(); + } + +} diff --git a/spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/hibernate/HibernateUtils.java b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/hibernate/HibernateUtils.java new file mode 100644 index 00000000..029ce1f6 --- /dev/null +++ b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/hibernate/HibernateUtils.java @@ -0,0 +1,43 @@ +package org.springframework.webflow.jpa.hibernate; + +import org.hibernate.SessionFactory; +import org.hibernate.EntityMode; +import org.hibernate.metadata.ClassMetadata; +import org.hibernate.classic.Session; +import org.hibernate.ejb.HibernateEntityManagerFactory; +import org.hibernate.ejb.HibernateEntityManager; + +import javax.persistence.EntityManagerFactory; +import javax.persistence.EntityManager; +import java.util.Map; +import java.util.List; +import java.util.LinkedList; + +/** + * Utils class for common hibernate JPA routines. + * + * @author Maxim Petrashev + */ +public abstract class HibernateUtils { + public static SessionFactory getSessionFactory(EntityManagerFactory aEntityManagerFactory) { + HibernateEntityManagerFactory hibernateEntityManagerFactory + = (HibernateEntityManagerFactory) aEntityManagerFactory; + return hibernateEntityManagerFactory.getSessionFactory(); + } + public static Session getSession(EntityManager aEntityManager) { + return (Session) ((HibernateEntityManager)aEntityManager).getSession(); + } + @Deprecated public static List getEntityClasses( EntityManager aEntityManager ) { + SessionFactory sessionFactory = getSession(aEntityManager).getSessionFactory(); + Map metadataMap = sessionFactory.getAllClassMetadata(); + List retVal = new LinkedList(); + for (ClassMetadata classMetadata : metadataMap.values()) { + Class type = classMetadata.getMappedClass(EntityMode.POJO); + retVal.add( type ); + } + return retVal; + } + public static Object getIdentifier(SessionFactory aSessionFactory, Object aEntity) { + return aSessionFactory.getCurrentSession().getIdentifier( aEntity ); + } +} diff --git a/spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/Sale.java b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/Sale.java new file mode 100644 index 00000000..01fce276 --- /dev/null +++ b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/Sale.java @@ -0,0 +1,174 @@ +/* + * 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.samples.sellitem; + +import java.io.Serializable; +import java.util.Date; + +import org.springframework.core.style.ToStringCreator; +import org.hibernate.annotations.Proxy; + +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.Id; +import javax.persistence.TableGenerator; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Transient; + +@Entity +@Table(name = "T_SALES") +@Proxy(lazy=true) +public class Sale implements Serializable { + + @Id + @TableGenerator( name="ids" ) + @GeneratedValue(strategy= GenerationType.TABLE) + public int getId() { + return id; + } + + public void setId(int aId) { + id = aId; + } + + private int id; + + private double price; + + private int itemCount; + + private String category; + + private boolean shipping; + + private String shippingType; + + private Date shipDate; + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public int getItemCount() { + return itemCount; + } + + public void setItemCount(int itemCount) { + this.itemCount = itemCount; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + public boolean isShipping() { + return shipping; + } + + public void setShipping(boolean shipping) { + this.shipping = shipping; + } + + public String getShippingType() { + return shippingType; + } + + public void setShippingType(String shippingType) { + this.shippingType = shippingType; + } + + public Date getShipDate() { + return shipDate; + } + + public void setShipDate(Date shipDate) { + this.shipDate = shipDate; + } + + // business logic methods + + /** + * Returns the base amount of the sale, without discount or delivery costs. + */ + @Transient + public double getAmount() { + return price * itemCount; + } + + /** + * Returns the discount rate to apply. + */ + @Transient + public double getDiscountRate() { + double discount = 0.02; + if ("A".equals(category)) { + if (itemCount >= 100) { + discount = 0.1; + } + } + else if ("B".equals(category)) { + if (itemCount >= 200) { + discount = 0.2; + } + } + return discount; + } + + /** + * Returns the savings because of the discount. + */ + @Transient + public double getSavings() { + return getDiscountRate() * getAmount(); + } + + /** + * Returns the delivery cost. + */ + @Transient + public double getDeliveryCost() { + double delCost = 0.0; + if ("S".equals(shippingType)) { + delCost = 10.0; + } + else if ("E".equals(shippingType)) { + delCost = 20.0; + } + return delCost; + } + + /** + * Returns the total cost of the sale, including discount and delivery cost. + */ + @Transient + public double getTotalCost() { + return getAmount() + getDeliveryCost() - getSavings(); + } + + public String toString() { + return new ToStringCreator(this).append("price", price).append("itemCount", itemCount).append("shippingType", + shippingType).append("shipDate", shipDate).toString(); + } +} \ No newline at end of file diff --git a/spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/SaleProcessor.java b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/SaleProcessor.java new file mode 100644 index 00000000..34692766 --- /dev/null +++ b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/SaleProcessor.java @@ -0,0 +1,24 @@ +/* + * 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.samples.sellitem; + +import org.springframework.transaction.annotation.Transactional; + +@Transactional +public interface SaleProcessor { + + public void process(Sale sale); +} diff --git a/spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/SaleValidator.java b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/SaleValidator.java new file mode 100644 index 00000000..51cfcef5 --- /dev/null +++ b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/SaleValidator.java @@ -0,0 +1,41 @@ +/* + * 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.samples.sellitem; + +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +//todo remove dependency on Validator interface +public class SaleValidator implements Validator { + + public boolean supports(Class clazz) { + return Sale.class.equals(clazz); + } + + public void validate(Object obj, Errors errors) { + Sale sale = (Sale)obj; + validatePriceAndItemCount(sale, errors); + } + + public void validatePriceAndItemCount(Sale sale, Errors errors) { + if (sale.getItemCount() <= 0) { + errors.rejectValue("itemCount", "tooLittle", "Item count must be greater than 0"); + } + if (sale.getPrice() <= 0.0) { + errors.rejectValue("price", "tooLittle", "Price must be greater than 0.0"); + } + } +} \ No newline at end of file diff --git a/spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/SellItemFlowExecutionListener.java b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/SellItemFlowExecutionListener.java new file mode 100644 index 00000000..91467324 --- /dev/null +++ b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/SellItemFlowExecutionListener.java @@ -0,0 +1,40 @@ +/* + * 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.samples.sellitem; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.util.StringUtils; +import org.springframework.webflow.context.servlet.ServletExternalContext; +import org.springframework.webflow.definition.StateDefinition; +import org.springframework.webflow.execution.EnterStateVetoException; +import org.springframework.webflow.execution.FlowExecutionListenerAdapter; +import org.springframework.webflow.execution.RequestContext; + +public class SellItemFlowExecutionListener extends FlowExecutionListenerAdapter { + + public void stateEntering(RequestContext context, StateDefinition nextState) throws EnterStateVetoException { + String role = nextState.getAttributes().getString("role"); + if (StringUtils.hasText(role)) { + HttpServletRequest request = ((ServletExternalContext)context.getExternalContext()).getRequest();//todo remove dependency on servlet api + if (!request.isUserInRole(role)) { + throw new EnterStateVetoException(context.getActiveFlow().getId(), context.getCurrentState().getId(), + nextState.getId(), "State requires role '" + role + + "', but the authenticated user doesn't have it!"); + } + } + } +} diff --git a/spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/SellItemPropertyEditorRegistrar.java b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/SellItemPropertyEditorRegistrar.java new file mode 100644 index 00000000..7b2e67d2 --- /dev/null +++ b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/SellItemPropertyEditorRegistrar.java @@ -0,0 +1,30 @@ +/* + * 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.samples.sellitem; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.springframework.beans.PropertyEditorRegistrar; +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.beans.propertyeditors.CustomDateEditor; + +public class SellItemPropertyEditorRegistrar implements PropertyEditorRegistrar { + + public void registerCustomEditors(PropertyEditorRegistry registry) { + registry.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("MM/dd/yyyy"), true)); + } +} diff --git a/spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/jpa/JpaSaleProcessor.java b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/jpa/JpaSaleProcessor.java new file mode 100644 index 00000000..1e54c3a4 --- /dev/null +++ b/spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/jpa/JpaSaleProcessor.java @@ -0,0 +1,14 @@ +package org.springframework.webflow.samples.sellitem.jpa; + +import org.springframework.webflow.samples.sellitem.SaleProcessor; +import org.springframework.webflow.samples.sellitem.Sale; + +import javax.persistence.EntityManager; + +public abstract class JpaSaleProcessor implements SaleProcessor { + public void process(Sale sale) { + EntityManager entityManager = getEntityManager(); + entityManager.persist(sale); + } + protected abstract EntityManager getEntityManager(); +} diff --git a/spring-webflow-sandbox/src/main/webapp/WEB-INF/classes/META-INF/persistence.xml b/spring-webflow-sandbox/src/main/webapp/WEB-INF/classes/META-INF/persistence.xml new file mode 100644 index 00000000..a609ee3c --- /dev/null +++ b/spring-webflow-sandbox/src/main/webapp/WEB-INF/classes/META-INF/persistence.xml @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/spring-webflow-sandbox/src/main/webapp/WEB-INF/classes/log4j.properties b/spring-webflow-sandbox/src/main/webapp/WEB-INF/classes/log4j.properties new file mode 100644 index 00000000..5ba9222c --- /dev/null +++ b/spring-webflow-sandbox/src/main/webapp/WEB-INF/classes/log4j.properties @@ -0,0 +1,9 @@ +log4j.rootCategory=WARN, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +# Enable web flow logging +log4j.category.org.springframework.webflow=DEBUG +log4j.category.org.springframework.binding=DEBUG \ No newline at end of file diff --git a/spring-webflow-sandbox/src/main/webapp/WEB-INF/classes/org/springframework/webflow/samples/sellitem/services-config.xml b/spring-webflow-sandbox/src/main/webapp/WEB-INF/classes/org/springframework/webflow/samples/sellitem/services-config.xml new file mode 100644 index 00000000..a21c5989 --- /dev/null +++ b/spring-webflow-sandbox/src/main/webapp/WEB-INF/classes/org/springframework/webflow/samples/sellitem/services-config.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.hibernate.dialect.HSQLDialect + create-drop + org.hsqldb.jdbcDriver + jdbc:hsqldb:mem:sellItem + after_transaction + true + org.hibernate.context.ManagedSessionContext + + + + \ No newline at end of file diff --git a/spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/conversation-scope/sellitem-beans.xml b/spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/conversation-scope/sellitem-beans.xml new file mode 100644 index 00000000..66a4c1cc --- /dev/null +++ b/spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/conversation-scope/sellitem-beans.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/conversation-scope/sellitem-conversation-scope-flow.xml b/spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/conversation-scope/sellitem-conversation-scope-flow.xml new file mode 100644 index 00000000..b6850a3f --- /dev/null +++ b/spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/conversation-scope/sellitem-conversation-scope-flow.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/conversation-scope/shipping-conversation-scope-flow.xml b/spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/conversation-scope/shipping-conversation-scope-flow.xml new file mode 100644 index 00000000..2f12345f --- /dev/null +++ b/spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/conversation-scope/shipping-conversation-scope-flow.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/sellitem-beans.xml b/spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/sellitem-beans.xml new file mode 100644 index 00000000..2b828373 --- /dev/null +++ b/spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/sellitem-beans.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/sellitem-flow.xml b/spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/sellitem-flow.xml new file mode 100644 index 00000000..a49fb3d2 --- /dev/null +++ b/spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/sellitem-flow.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/shipping-flow.xml b/spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/shipping-flow.xml new file mode 100644 index 00000000..9bf52e40 --- /dev/null +++ b/spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/shipping-flow.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/simple/sellitem-simple-flow.xml b/spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/simple/sellitem-simple-flow.xml new file mode 100644 index 00000000..8c06eaa1 --- /dev/null +++ b/spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/simple/sellitem-simple-flow.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/categoryForm.jsp b/spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/categoryForm.jsp new file mode 100644 index 00000000..6d132f90 --- /dev/null +++ b/spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/categoryForm.jsp @@ -0,0 +1,48 @@ +<%@ include file="includeTop.jsp" %> + +

+
+

Select category

+ + + + + + + + + + + + + + + + + + + + +
Price:${sale.price}
Item count:${sale.itemCount}
Category: + + + +
Is shipping required?: + +
+ + +
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/costOverview.jsp b/spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/costOverview.jsp new file mode 100644 index 00000000..8dd43e98 --- /dev/null +++ b/spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/costOverview.jsp @@ -0,0 +1,71 @@ +<%@ include file="includeTop.jsp" %> + +
+
+

Purchase cost overview

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Price:${sale.price}
Item count:${sale.itemCount}
Category:${sale.category}
Shipping Info: + + + + + + + + + + + +
Type:${sale.shippingType}
Date: + + ${status.value} + +
+
+ + No shipping required: you're picking up the items + +
+
Base amount:${sale.amount}
Delivery cost:${sale.deliveryCost}
Discount:${sale.savings} (Discount rate: ${sale.discountRate})

Total cost:${sale.totalCost}
+
"> + +
+
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/error.jsp b/spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/error.jsp new file mode 100644 index 00000000..65d8ab8c --- /dev/null +++ b/spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/error.jsp @@ -0,0 +1,17 @@ +<%@ include file="includeTop.jsp" %> + +
+
+ +
+

+ + Duplicate submit of the same transaction not allowed! + +

+

+ Sell a new item +

+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/includeBottom.jsp b/spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/includeBottom.jsp new file mode 100644 index 00000000..dfe41438 --- /dev/null +++ b/spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/includeBottom.jsp @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/includeTop.jsp b/spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/includeTop.jsp new file mode 100644 index 00000000..416cad5f --- /dev/null +++ b/spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/includeTop.jsp @@ -0,0 +1,22 @@ +<%@ page contentType="text/html" %> +<%@ page session="false" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> + + + +Sell an item + + + + + + + + diff --git a/spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/priceAndItemCountForm.jsp b/spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/priceAndItemCountForm.jsp new file mode 100644 index 00000000..ab220892 --- /dev/null +++ b/spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/priceAndItemCountForm.jsp @@ -0,0 +1,27 @@ +<%@ include file="includeTop.jsp" %> + +
+
+

Enter price and item count

+
+ + + + + + + + + + + + + + +
Price:
Item count:
+ + +
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/shippingDetailsForm.jsp b/spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/shippingDetailsForm.jsp new file mode 100644 index 00000000..e6c9a61c --- /dev/null +++ b/spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/shippingDetailsForm.jsp @@ -0,0 +1,51 @@ +<%@ include file="includeTop.jsp" %> + +
+
+

Enter shipping information

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Price:${sale.price}
Item count:${sale.itemCount}
Category:${sale.category}
Shipping:${sale.shipping}
Shipping type: + + + +
Ship date (DD/MM/YYYY): + +
+ + +
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-sandbox/src/main/webapp/WEB-INF/sellitem-servlet-config.xml b/spring-webflow-sandbox/src/main/webapp/WEB-INF/sellitem-servlet-config.xml new file mode 100644 index 00000000..bf8369dc --- /dev/null +++ b/spring-webflow-sandbox/src/main/webapp/WEB-INF/sellitem-servlet-config.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-sandbox/src/main/webapp/WEB-INF/sellitem-webflow-config.xml b/spring-webflow-sandbox/src/main/webapp/WEB-INF/sellitem-webflow-config.xml new file mode 100644 index 00000000..939ffb25 --- /dev/null +++ b/spring-webflow-sandbox/src/main/webapp/WEB-INF/sellitem-webflow-config.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-sandbox/src/main/webapp/WEB-INF/web.xml b/spring-webflow-sandbox/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..7cba7ad3 --- /dev/null +++ b/spring-webflow-sandbox/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,54 @@ + + + + + + webAppRootKey + swf-sellitem.root + + + + contextConfigLocation + + classpath:org/springframework/webflow/samples/sellitem/services-config.xml + + + + + log4jConfigLocation + /WEB-INF/classes/log4j.properties + + + + org.springframework.web.context.ContextLoaderListener + + + + org.springframework.web.util.Log4jConfigListener + + + + sellitem + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + + /WEB-INF/sellitem-servlet-config.xml + /WEB-INF/sellitem-webflow-config.xml + + + + + + sellitem + *.htm + + + + index.jsp + + + \ No newline at end of file diff --git a/spring-webflow-sandbox/src/main/webapp/images/spring-logo.jpg b/spring-webflow-sandbox/src/main/webapp/images/spring-logo.jpg new file mode 100644 index 00000000..62be3983 Binary files /dev/null and b/spring-webflow-sandbox/src/main/webapp/images/spring-logo.jpg differ diff --git a/spring-webflow-sandbox/src/main/webapp/images/webflow-logo.jpg b/spring-webflow-sandbox/src/main/webapp/images/webflow-logo.jpg new file mode 100644 index 00000000..ed76bae0 Binary files /dev/null and b/spring-webflow-sandbox/src/main/webapp/images/webflow-logo.jpg differ diff --git a/spring-webflow-sandbox/src/main/webapp/index.jsp b/spring-webflow-sandbox/src/main/webapp/index.jsp new file mode 100644 index 00000000..45801400 --- /dev/null +++ b/spring-webflow-sandbox/src/main/webapp/index.jsp @@ -0,0 +1,58 @@ +<%@ page session="true" %> <%-- make sure we have a session --%> + + +
Sell Item - A Spring Web Flow Sample
+ +
+ +
+

+ Sell Item +

+ +

+ This Spring Web Flow sample application implements the example application + discussed in the article + + Use continuations to develop complex Web applications. It illustrates + the following concepts: +

    +
  • + Using the "_flowId" request parameter to let the view tell the web + flow controller which flow needs to be started. +
  • +
  • + Implementing a wizard using web flows. +
  • +
  • + Use of the FormAction to perform form processing, including the + FormAction's "setupForm" method to install custom property editors for + formatting text field values (shipDate). +
  • +
  • + Using continuations to make the flow completely stable, no matter + how browser navigation buttons are used. +
  • +
  • + Using "conversation invalidation after completion" to prevent duplicate submits + of the same sale while taking advantage of continuations to allow back button + usage while the application transaction is in process. +
  • +
  • + "Always redirect on pause" to benefit from the POST+REDIRECT+GET pattern with no special coding. +
  • +
  • + Using OGNL based conditional expressions. +
  • +
  • + Use of subflows to compose a multi-step business process from independently reusable modules. +
  • +
+

+
+ +
+ +
+ + diff --git a/spring-webflow-sandbox/src/main/webapp/style.css b/spring-webflow-sandbox/src/main/webapp/style.css new file mode 100644 index 00000000..f4b0a64e --- /dev/null +++ b/spring-webflow-sandbox/src/main/webapp/style.css @@ -0,0 +1,58 @@ +body { + width: 720px; + margin: 0px; + padding: 0px; +} + +div#logo { + width: 720px; + height: 73px; + background: #86AEA5; +} + +div#navigation { + width: 720px; + height: 15px; + background: #E2F3B8; + text-align: right; +} + +div#content { + width: 720px; + padding: 5px; +} + +div#insert { + width: 120; + float: right; + text-align: right; +} + +.buttonBar { + height: 1.5em; + text-align: right; +} + +div#copyright { + width: 720px; +} + +div#copyright p { + text-align: center; + font-family: Tahoma, sans-serif; + font-size: 75%; + color: div#336633; + margin-left: 5px; + font-weight: bold; + clear: both; +} + +.readOnly { + color: rgb(192, 192, 192); +} + +.error { + color: red; + font-weight: bold; + font-family: Arial, sans-serif; +} \ No newline at end of file