From 21823a2e652def9eb4e071fe46bcae035398d80f Mon Sep 17 00:00:00 2001 From: Maxim Petrashev Date: Wed, 7 Mar 2007 14:50:42 +0000 Subject: [PATCH] Initial import --- spring-webflow-sandbox/build.xml | 2 + spring-webflow-sandbox/ivy.xml | 11 ++ spring-webflow-sandbox/project.properties | 2 +- .../src/etc/test-resources/log4j.properties | 1 - .../util/AbstractReadOnlyResourceHolder.java | 12 ++ .../util/AbstractResourceHolder.java | 23 +++ .../springframework/util/ResourceHolder.java | 11 ++ .../ConversationLifecycleListener.java | 96 ++++++++++ .../ConversationLifecycleListenerAdapter.java | 44 +++++ ...endedFlowExecutionListenerInterceptor.java | 181 ++++++++++++++++++ ...faultEntityManagerLifecycleController.java | 71 +++++++ .../jpa/EntityManagerLifecycleController.java | 43 +++++ ...ityManagerPerConversationFlowListener.java | 143 ++++++++++++++ ...chronizationManagerEnityManagerHolder.java | 25 +++ .../EntityManagerLifecycleController.java | 68 +++++++ .../webflow/jpa/hibernate/HibernateUtils.java | 43 +++++ .../webflow/samples/sellitem/Sale.java | 174 +++++++++++++++++ .../samples/sellitem/SaleProcessor.java | 24 +++ .../samples/sellitem/SaleValidator.java | 41 ++++ .../SellItemFlowExecutionListener.java | 40 ++++ .../SellItemPropertyEditorRegistrar.java | 30 +++ .../sellitem/jpa/JpaSaleProcessor.java | 14 ++ .../WEB-INF/classes/META-INF/persistence.xml | 6 + .../webapp/WEB-INF/classes/log4j.properties | 9 + .../samples/sellitem/services-config.xml | 50 +++++ .../conversation-scope/sellitem-beans.xml | 20 ++ .../sellitem-conversation-scope-flow.xml | 54 ++++++ .../shipping-conversation-scope-flow.xml | 19 ++ .../webapp/WEB-INF/flows/sellitem-beans.xml | 18 ++ .../webapp/WEB-INF/flows/sellitem-flow.xml | 59 ++++++ .../webapp/WEB-INF/flows/shipping-flow.xml | 23 +++ .../flows/simple/sellitem-simple-flow.xml | 56 ++++++ .../main/webapp/WEB-INF/jsp/categoryForm.jsp | 48 +++++ .../main/webapp/WEB-INF/jsp/costOverview.jsp | 71 +++++++ .../src/main/webapp/WEB-INF/jsp/error.jsp | 17 ++ .../main/webapp/WEB-INF/jsp/includeBottom.jsp | 5 + .../main/webapp/WEB-INF/jsp/includeTop.jsp | 22 +++ .../WEB-INF/jsp/priceAndItemCountForm.jsp | 27 +++ .../WEB-INF/jsp/shippingDetailsForm.jsp | 51 +++++ .../WEB-INF/sellitem-servlet-config.xml | 22 +++ .../WEB-INF/sellitem-webflow-config.xml | 36 ++++ .../src/main/webapp/WEB-INF/web.xml | 54 ++++++ .../src/main/webapp/images/spring-logo.jpg | Bin 0 -> 31339 bytes .../src/main/webapp/images/webflow-logo.jpg | Bin 0 -> 2383 bytes .../src/main/webapp/index.jsp | 58 ++++++ .../src/main/webapp/style.css | 58 ++++++ 46 files changed, 1880 insertions(+), 2 deletions(-) create mode 100644 spring-webflow-sandbox/src/main/java/org/springframework/util/AbstractReadOnlyResourceHolder.java create mode 100644 spring-webflow-sandbox/src/main/java/org/springframework/util/AbstractResourceHolder.java create mode 100644 spring-webflow-sandbox/src/main/java/org/springframework/util/ResourceHolder.java create mode 100644 spring-webflow-sandbox/src/main/java/org/springframework/webflow/execution/ConversationLifecycleListener.java create mode 100644 spring-webflow-sandbox/src/main/java/org/springframework/webflow/execution/ConversationLifecycleListenerAdapter.java create mode 100644 spring-webflow-sandbox/src/main/java/org/springframework/webflow/execution/ExtendedFlowExecutionListenerInterceptor.java create mode 100644 spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/DefaultEntityManagerLifecycleController.java create mode 100644 spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/EntityManagerLifecycleController.java create mode 100644 spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/OpenEntityManagerPerConversationFlowListener.java create mode 100644 spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/TransactionSynchronizationManagerEnityManagerHolder.java create mode 100644 spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/hibernate/EntityManagerLifecycleController.java create mode 100644 spring-webflow-sandbox/src/main/java/org/springframework/webflow/jpa/hibernate/HibernateUtils.java create mode 100644 spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/Sale.java create mode 100644 spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/SaleProcessor.java create mode 100644 spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/SaleValidator.java create mode 100644 spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/SellItemFlowExecutionListener.java create mode 100644 spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/SellItemPropertyEditorRegistrar.java create mode 100644 spring-webflow-sandbox/src/main/java/org/springframework/webflow/samples/sellitem/jpa/JpaSaleProcessor.java create mode 100644 spring-webflow-sandbox/src/main/webapp/WEB-INF/classes/META-INF/persistence.xml create mode 100644 spring-webflow-sandbox/src/main/webapp/WEB-INF/classes/log4j.properties create mode 100644 spring-webflow-sandbox/src/main/webapp/WEB-INF/classes/org/springframework/webflow/samples/sellitem/services-config.xml create mode 100644 spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/conversation-scope/sellitem-beans.xml create mode 100644 spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/conversation-scope/sellitem-conversation-scope-flow.xml create mode 100644 spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/conversation-scope/shipping-conversation-scope-flow.xml create mode 100644 spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/sellitem-beans.xml create mode 100644 spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/sellitem-flow.xml create mode 100644 spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/shipping-flow.xml create mode 100644 spring-webflow-sandbox/src/main/webapp/WEB-INF/flows/simple/sellitem-simple-flow.xml create mode 100644 spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/categoryForm.jsp create mode 100644 spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/costOverview.jsp create mode 100644 spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/error.jsp create mode 100644 spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/includeBottom.jsp create mode 100644 spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/includeTop.jsp create mode 100644 spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/priceAndItemCountForm.jsp create mode 100644 spring-webflow-sandbox/src/main/webapp/WEB-INF/jsp/shippingDetailsForm.jsp create mode 100644 spring-webflow-sandbox/src/main/webapp/WEB-INF/sellitem-servlet-config.xml create mode 100644 spring-webflow-sandbox/src/main/webapp/WEB-INF/sellitem-webflow-config.xml create mode 100644 spring-webflow-sandbox/src/main/webapp/WEB-INF/web.xml create mode 100644 spring-webflow-sandbox/src/main/webapp/images/spring-logo.jpg create mode 100644 spring-webflow-sandbox/src/main/webapp/images/webflow-logo.jpg create mode 100644 spring-webflow-sandbox/src/main/webapp/index.jsp create mode 100644 spring-webflow-sandbox/src/main/webapp/style.css 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 0000000000000000000000000000000000000000..62be39835f2d980befae3ec961b6b10e7d1fca6f GIT binary patch literal 31339 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%0Dgs02_0)mtC0O8J0`s=@(mN;yyuh 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 HzQ6z3m$6-^ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ed76bae066c665b63369a970cb47b9a038ceaee0 GIT binary patch literal 2383 zcmb7^dpy$%AIE>&%vf$i9HP0cHp^1E9jRCi8&0e>WxBgbhU-}$rtMxH_EBhNY@J`7m%0P*i{_ zLKPGhp^7k=q7qyK4p&x&YpbcNXy|JH^pmc(j*h+|3aP)v)IdkaXor#Mw(S^mj2_a$ z(gJOXLSxWBN?cbAPA7W6EFY(Fc<;? z0sj>YstATCfRr|NC5-^Lm|-r{QY4@C_puhcX?@HzH?q18) zylwd|M+c_Pdov=@TH)!V^S+evc@sV_ipx69k;u}BF5E1Gx1MqU%3eSEtT*^;(7H*z zG*s7R6Y;>`AOZJSAN_MMPtT?Np2d|T;`{aP%BEt?-GXWGHLb}Tt7$?;Hkqm7iV5Gt z$MNQjJF#%51^AZKDtfuUxnv>C+P*H=>n$U}5|$a!vHmD#?BQ%p%Dh^y;%n?1i`lot z3<7W!AUnar2%9VaB4~Tz5rnN)o*eSw>hmKZ_hl7wU`VzXgA@n1{J=DAqV&xk{$HT}TK3@jYSJ=sK&Txj$ViV$uHOH>q zp~-{d2V9-%;K$P;##v!&z;KtHRgL7oqbUO;78XE zG3zDF*Rd~J`-fVQS-n%G$*PSF!t`HxOgbPrnaetrv2P6VRV#^HPWRY95GKu*z`E_# zC$Wc^Zm_g>Zh7sRFbzle^!4$GwCCfDT*}9u#DMaNBD9O;qvnsw-Qf|miq7uTV0vUf zT>lrYsM20+QhmR+J%)s|8vWZ)RJ9mRwXoH_;M$h4_tWp*9o!LQ%ojnLVL6JCK8R#q z#WY~4A5@~!yV?yxBDDAKhqT6u$T{voeC?&nS?PY|k`X+OzU6P3b$H-m2GuvcZ*uhV z>R|Ujy0h);E;(Uw4u9V8T`24>(DQo#V0fxjsFRoBGm@1EyXDh86%_OfbZ1aesz0{t z17~)e+hKjQD%kG6z29qb4H5}x3Fe9v~!sJ+6JNxd)2StCX7Zv^M@#4;GcsG!_O~SbgEQ z9s0EE)ahBF!OQ24-}7cVCY-vCk4N3QhHpU&ID0y&ww;Dr3Yux`f&_W#{;HUF7gJU&qX*P@!j#ag(n#| ziw>AktRMR@Xo+Y_w^4PRam~m&WBWXq*Cae89D8u`v-h)ryB73Xw3NGxl-aqv6I`B;XqaWC3VL3wlROcWvV@CbaB2Ax3uhKUqzy^txo3( zEP6xYwR37&Ed?oG4gVx04A_2i;A7l{afpFKesczw_QbpNndD>Z@duS+!iU_jcBCiW z%3t+OC8NEO)>~<6bWD^1+g6EIHUv7yoFeZkQ+a$x(1!<%Fc zBuT-s#y!j^$2X)fPFY;H>cNIL&Drc5#y4I4vv*S@ui>l(FKzzyNqlDlhbWkUnoir| zY#(qYwud%bx0|&Xf2lCUvffSQh>7G3-WHK~XW%0Q|MESN@$sR()?JYKu`XXGwU3aC zx31jb%JqB!Au5xvP11*VoQ|P%t;RlkA_x3xrlJp>km)|W|1feJIvkTOVH?zi7nlBe zZ0KT>{fx(?T1V_V#<=PSt_8_IG()JP%#Y|=$vFCP^y+VODY^YPA=WdqH?Gpa#Y*L7 zU|cK}ecQ10?LnEqb|$k>x+oYKnH;8lEYoMJ+ZfR4KI#Os34L=XDCu7CBJRa*MP7P7 z%F<2UEZ$GRT-Gn`<=V9C`DarWzuPf83fP0#h-I{h)oI*0TX$0sgSa#2VN;ZEW;VN4 zZ@AB#8acGpw6CLdb!J*~t=hG0xcXbFxtpthuEigpoR1{l$Sxdn_*~^otTMj@2^Ht% zm(4e&6U=anbddIVE z)yzlkU=p#>4<6RJse#h*-=uQj0Q-nYC)Do2KiQqoN*_{^+p&{jmc+9ATIX6{h2`f= z`UE+9ntSJ32fHf&iAsgq&~B8))@1`S0#W`Ek4?r)4%yW z@N7q2#?9fBN^Hqv2Xw@yN5Y(voj!R<7hK0;EY|k6#)qV}*e7S{94)zxD55kp5J`gt z?5E2a5|7!Ma;nCJ$)t Pr%*S>8-PL?@;Cnig|{u< literal 0 HcmV?d00001 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