view-scope polishing

This commit is contained in:
Keith Donald
2008-03-31 16:13:25 +00:00
parent a1446d3e6c
commit 3cf6ddfb61
11 changed files with 164 additions and 75 deletions

View File

@@ -117,14 +117,4 @@ public interface RequestControlContext extends RequestContext {
*/
public boolean getAlwaysRedirectOnPause();
/**
* Initialize view scope. Called by view states when they enter.
*/
public void initViewScope();
/**
* Destroy view-scope. Called by view-states when they exit.
*/
public void destroyViewScope();
}

View File

@@ -162,7 +162,6 @@ public class ViewState extends TransitionableState {
}
protected void doPreEntryActions(RequestControlContext context) throws FlowExecutionException {
context.initViewScope();
createVariables(context);
}
@@ -203,7 +202,6 @@ public class ViewState extends TransitionableState {
public void exit(RequestControlContext context) {
destroyVariables(context);
context.destroyViewScope();
super.exit(context);
}

View File

@@ -331,6 +331,9 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
void start(Flow flow, MutableAttributeMap input, RequestControlContext context) {
listeners.fireSessionCreating(context, flow);
FlowSession session = activateSession(flow);
if (input == null) {
input = new LocalAttributeMap();
}
listeners.fireSessionStarting(context, session, input);
flow.start(context, input);
listeners.fireSessionStarted(context, session);

View File

@@ -40,6 +40,8 @@ import org.springframework.webflow.execution.FlowSession;
*/
class FlowSessionImpl implements FlowSession, Externalizable {
private static final String FLOW_VIEW_MAP_ATTRIBUTE = "flowViewMap";
/**
* The flow definition (a singleton).
* <p>
@@ -104,6 +106,18 @@ class FlowSessionImpl implements FlowSession, Externalizable {
return scope;
}
public MutableAttributeMap getViewScope() throws IllegalStateException {
if (state == null) {
throw new IllegalStateException("The current state of this flow '" + flow.getId()
+ "' is [null] - cannot access view scope");
}
if (!state.isViewState()) {
throw new IllegalStateException("The current state '" + state.getId() + "' of this flow '" + flow.getId()
+ "' is not a view state - view scope not accessible");
}
return (MutableAttributeMap) scope.get(FLOW_VIEW_MAP_ATTRIBUTE);
}
public FlowSession getParent() {
return parent;
}
@@ -158,8 +172,14 @@ class FlowSessionImpl implements FlowSession, Externalizable {
Assert.notNull(state, "The state is required");
Assert.isTrue(flow == state.getOwner(),
"The state does not belong to the flow associated with this flow session");
if (this.state != null && this.state.isViewState()) {
destroyViewScope();
}
this.state = state;
this.stateId = state.getId();
if (this.state.isViewState()) {
initViewScope();
}
}
/**
@@ -176,6 +196,16 @@ class FlowSessionImpl implements FlowSession, Externalizable {
return stateId;
}
// internal helpers
private void initViewScope() {
scope.put(FLOW_VIEW_MAP_ATTRIBUTE, new LocalAttributeMap());
}
private void destroyViewScope() {
scope.remove(FLOW_VIEW_MAP_ATTRIBUTE);
}
public String toString() {
return new ToStringCreator(this).append("flow", flowId).append("state", stateId).append("scope", scope)
.toString();

View File

@@ -48,8 +48,6 @@ import org.springframework.webflow.execution.FlowSession;
*/
class RequestControlContextImpl implements RequestControlContext {
private static final String FLOW_VIEW_MAP_ATTRIBUTE = "flowViewMap";
/**
* The owning flow execution carrying out this request.
*/
@@ -123,14 +121,7 @@ class RequestControlContextImpl implements RequestControlContext {
}
public MutableAttributeMap getViewScope() throws IllegalStateException {
if (!flowExecution.isActive()) {
throw new IllegalStateException("This flow is not active");
}
if (!getCurrentState().isViewState()) {
throw new IllegalStateException("The current state '" + getCurrentState().getId() + "' of this flow '"
+ getActiveFlow().getId() + "' is not a view state - view scope not accessible");
}
return (MutableAttributeMap) getFlowScope().get(FLOW_VIEW_MAP_ATTRIBUTE);
return flowExecution.getActiveSession().getViewScope();
}
public MutableAttributeMap getFlowScope() {
@@ -213,14 +204,6 @@ class RequestControlContextImpl implements RequestControlContext {
flowExecution.start(flow, input, this);
}
public void initViewScope() {
getFlowScope().put(FLOW_VIEW_MAP_ATTRIBUTE, new LocalAttributeMap());
}
public void destroyViewScope() {
getFlowScope().remove(FLOW_VIEW_MAP_ATTRIBUTE);
}
public boolean handleEvent(Event event) throws FlowExecutionException {
this.lastEvent = event;
return flowExecution.handleEvent(event, this);

View File

@@ -100,8 +100,8 @@ public interface FlowExecutionContext {
public FlowSession getActiveSession() throws IllegalStateException;
/**
* Returns a mutable map for data held in "flash scope". Attributes in this map are cleared out on the next event
* signaled against this flow execution. Flash attributes survive flow execution refresh operations.
* Returns a mutable map for data held in "flash scope". Attributes in this map are cleared out on the next view
* rendering. Flash attributes survive flow execution refresh operations.
* @return flash scope
*/
public MutableAttributeMap getFlashScope();

View File

@@ -54,6 +54,14 @@ public interface FlowSession {
*/
public MutableAttributeMap getScope();
/**
* Returns a mutable map for data held in "view scope". Attributes in this map are cleared out when the current view
* state exits.
* @return view scope
* @throws IllegalStateException if this flow session is not currently in a view state
*/
public MutableAttributeMap getViewScope() throws IllegalStateException;
/**
* Returns the parent flow session in the current flow execution, or <code>null</code> if there is no parent flow
* session.

View File

@@ -36,14 +36,14 @@ import org.springframework.webflow.execution.FlowSession;
*/
public class MockFlowSession implements FlowSession {
private static final String FLOW_VIEW_MAP_ATTRIBUTE = "flowViewMap";
private Flow definition;
private State state;
private MutableAttributeMap scope = new LocalAttributeMap();
private MutableAttributeMap flashMap = new LocalAttributeMap();
private FlowSession parent;
/**
@@ -90,8 +90,16 @@ public class MockFlowSession implements FlowSession {
return scope;
}
public MutableAttributeMap getFlashMap() {
return flashMap;
public MutableAttributeMap getViewScope() throws IllegalStateException {
if (state == null) {
throw new IllegalStateException("The current state of this flow '" + definition.getId()
+ "' is [null] - cannot access view scope");
}
if (!state.isViewState()) {
throw new IllegalStateException("The current state '" + state.getId() + "' of this flow '"
+ definition.getId() + "' is not a view state - view scope not accessible");
}
return (MutableAttributeMap) scope.get(FLOW_VIEW_MAP_ATTRIBUTE);
}
public FlowSession getParent() {
@@ -115,7 +123,13 @@ public class MockFlowSession implements FlowSession {
* Set the currently active state.
*/
public void setState(State state) {
if (this.state != null && this.state.isViewState()) {
destroyViewScope();
}
this.state = state;
if (this.state != null && this.state.isViewState()) {
initViewScope();
}
}
/**
@@ -133,7 +147,7 @@ public class MockFlowSession implements FlowSession {
this.parent = parent;
}
// conveniece accessors
// convenience accessors
/**
* Returns the flow definition of this session.
@@ -148,4 +162,14 @@ public class MockFlowSession implements FlowSession {
public State getStateInternal() {
return state;
}
// internal helpers
private void initViewScope() {
scope.put(FLOW_VIEW_MAP_ATTRIBUTE, new LocalAttributeMap());
}
private void destroyViewScope() {
scope.remove(FLOW_VIEW_MAP_ATTRIBUTE);
}
}

View File

@@ -19,6 +19,7 @@ import junit.framework.TestCase;
import org.springframework.util.Assert;
import org.springframework.webflow.context.ExternalContext;
import org.springframework.webflow.core.collection.MutableAttributeMap;
import org.springframework.webflow.definition.FlowDefinition;
import org.springframework.webflow.engine.impl.FlowExecutionImplFactory;
import org.springframework.webflow.execution.FlowExecution;
@@ -33,9 +34,8 @@ import org.springframework.webflow.test.MockExternalContext;
* <p>
* More specifically, a typical flow execution test case will test:
* <ul>
* <li>That the flow execution starts as expected given a request from an external context containing potential input
* attributes (see the {@link #startFlow(ExternalContext)} variants).
* <li>That given the set of supported state transition criteria a state executes the appropriate transition when a
* <li>That the flow execution starts as expected (see {@link #startFlow(MutableAttributeMap, ExternalContext)}).
* <li>That given the set of supported state transition criteria, a state executes the appropriate transition when a
* matching event is signaled (with potential input request parameters, see the {@link #resumeFlow(ExternalContext)}
* variants). A test case should be coded for each logical event that can occur, where an event drives a possible path
* through the flow. The goal should be to exercise all possible paths of the flow. Use a test coverage tool like Clover
@@ -76,7 +76,6 @@ public abstract class AbstractFlowExecutionTests extends TestCase {
/**
* Constructs a flow execution test with given name.
* @param name the name of the test
* @since 1.0.2
*/
public AbstractFlowExecutionTests(String name) {
super(name);
@@ -101,9 +100,9 @@ public abstract class AbstractFlowExecutionTests extends TestCase {
* execution during the start operation
* @throws FlowExecutionException if an exception was thrown while starting the flow execution
*/
protected void startFlow(ExternalContext context) throws FlowExecutionException {
protected void startFlow(MutableAttributeMap input, ExternalContext context) throws FlowExecutionException {
flowExecution = getFlowExecutionFactory().createFlowExecution(getFlowDefinition());
flowExecution.start(null, context);
flowExecution.start(input, context);
}
/**
@@ -114,7 +113,7 @@ public abstract class AbstractFlowExecutionTests extends TestCase {
*/
protected void resumeFlow(ExternalContext context) throws FlowExecutionException {
Assert.state(flowExecution != null, "The flow execution to test is [null]; "
+ "you must start the flow execution before you can signal an event against it!");
+ "you must start the flow execution before you can resume it!");
flowExecution.resume(context);
}
@@ -127,40 +126,71 @@ public abstract class AbstractFlowExecutionTests extends TestCase {
*/
protected FlowExecution getFlowExecution() throws IllegalStateException {
Assert.state(flowExecution != null,
"The flow execution to test is [null]; you must start the flow execution before you can query it!");
"The flow execution to test is [null]; you must start the flow execution before you can access it!");
return flowExecution;
}
/**
* Returns the attribute in conversation scope. Conversation-scoped attributes are shared by all flow sessions.
* Returns the attribute in flash scope. Flash-scoped attributes are cleared on the next view rendering.
* @param attributeName the name of the attribute
* @return the attribute value
*/
protected Object getConversationAttribute(String attributeName) {
return getFlowExecution().getConversationScope().get(attributeName);
protected Object getFlashAttribute(String attributeName) {
return getFlowExecution().getFlashScope().get(attributeName);
}
/**
* Returns the required attribute in conversation scope; asserts the attribute is present. Conversation-scoped
* attributes are shared by all flow sessions.
* Returns the required attribute in flash scope; asserts the attribute is present. Flash-scoped attributes are
* cleared on the next view rendering.
* @param attributeName the name of the attribute
* @return the attribute value
* @throws IllegalStateException if the attribute was not present
*/
protected Object getRequiredConversationAttribute(String attributeName) throws IllegalStateException {
return getFlowExecution().getConversationScope().getRequired(attributeName);
protected Object getRequiredFlashAttribute(String attributeName) throws IllegalStateException {
return getFlowExecution().getFlashScope().getRequired(attributeName);
}
/**
* Returns the required attribute in conversation scope; asserts the attribute is present and of the required type.
* Conversation-scoped attributes are shared by all flow sessions.
* Returns the required attribute in flash scope; asserts the attribute is present and of the correct type.
* Flash-scoped attributes are cleared on the next view rendering.
* @param attributeName the name of the attribute
* @return the attribute value
* @throws IllegalStateException if the attribute was not present or not of the required type
* @throws IllegalStateException if the attribute was not present or was of the wrong type
*/
protected Object getRequiredConversationAttribute(String attributeName, Class requiredType)
throws IllegalStateException {
return getFlowExecution().getConversationScope().getRequired(attributeName, requiredType);
protected Object getRequiredFlashAttribute(String attributeName, Class requiredType) throws IllegalStateException {
return getFlowExecution().getFlashScope().getRequired(attributeName, requiredType);
}
/**
* Returns the attribute in view scope. View-scoped attributes are local to the current view state and are cleared
* when the view state exits.
* @param attributeName the name of the attribute
* @return the attribute value
*/
protected Object getViewAttribute(String attributeName) {
return getFlowExecution().getActiveSession().getViewScope().get(attributeName);
}
/**
* Returns the required attribute in view scope; asserts the attribute is present. View-scoped attributes are local
* to the current view state and are cleared when the view state exits.
* @param attributeName the name of the attribute
* @return the attribute value
* @throws IllegalStateException if the attribute was not present
*/
protected Object getRequiredViewAttribute(String attributeName) throws IllegalStateException {
return getFlowExecution().getActiveSession().getViewScope().getRequired(attributeName);
}
/**
* Returns the required attribute in view scope; asserts the attribute is present and of the correct type.
* View-scoped attributes are local to the current view state and are cleared when the view state exits.
* @param attributeName the name of the attribute
* @return the attribute value
* @throws IllegalStateException if the attribute was not present or was of the wrong type
*/
protected Object getRequiredViewAttribute(String attributeName, Class requiredType) throws IllegalStateException {
return getFlowExecution().getActiveSession().getViewScope().getRequired(attributeName, requiredType);
}
/**
@@ -195,35 +225,35 @@ public abstract class AbstractFlowExecutionTests extends TestCase {
}
/**
* Returns the attribute in flash scope. Flash-scoped attributes are local to the active flow session and cleared on
* the next user event.
* Returns the attribute in conversation scope. Conversation-scoped attributes are shared by all flow sessions.
* @param attributeName the name of the attribute
* @return the attribute value
*/
protected Object getFlashAttribute(String attributeName) {
return getFlowExecution().getFlashScope().get(attributeName);
protected Object getConversationAttribute(String attributeName) {
return getFlowExecution().getConversationScope().get(attributeName);
}
/**
* Returns the required attribute in flash scope; asserts the attribute is present. Flash-scoped attributes are
* local to the active flow session and cleared on the next user event.
* Returns the required attribute in conversation scope; asserts the attribute is present. Conversation-scoped
* attributes are shared by all flow sessions.
* @param attributeName the name of the attribute
* @return the attribute value
* @throws IllegalStateException if the attribute was not present
*/
protected Object getRequiredFlashAttribute(String attributeName) throws IllegalStateException {
return getFlowExecution().getFlashScope().getRequired(attributeName);
protected Object getRequiredConversationAttribute(String attributeName) throws IllegalStateException {
return getFlowExecution().getConversationScope().getRequired(attributeName);
}
/**
* Returns the required attribute in flash scope; asserts the attribute is present and of the correct type.
* Flash-scoped attributes are local to the active flow session and cleared on the next user event.
* Returns the required attribute in conversation scope; asserts the attribute is present and of the required type.
* Conversation-scoped attributes are shared by all flow sessions.
* @param attributeName the name of the attribute
* @return the attribute value
* @throws IllegalStateException if the attribute was not present or was of the wrong type
* @throws IllegalStateException if the attribute was not present or not of the required type
*/
protected Object getRequiredFlashAttribute(String attributeName, Class requiredType) throws IllegalStateException {
return getFlowExecution().getFlashScope().getRequired(attributeName, requiredType);
protected Object getRequiredConversationAttribute(String attributeName, Class requiredType)
throws IllegalStateException {
return getFlowExecution().getConversationScope().getRequired(attributeName, requiredType);
}
// assert helpers

View File

@@ -99,6 +99,29 @@ public class FlowExecutionImplTests extends TestCase {
assertEquals(1, mockListener.getPausedCount());
}
public void testStartWithNullInputMap() {
Flow flow = new Flow("flow");
new State(flow, "state") {
protected void doEnter(RequestControlContext context) throws FlowExecutionException {
// no op
}
};
MockFlowExecutionListener mockListener = new MockFlowExecutionListener() {
public void sessionStarting(RequestContext context, FlowSession session, MutableAttributeMap input) {
super.sessionStarting(context, session, input);
assertNotNull(input);
}
};
FlowExecutionListener[] listeners = new FlowExecutionListener[] { mockListener };
FlowExecutionImpl execution = new FlowExecutionImpl(flow);
execution.setListeners(listeners);
execution.setMessageContextFactory(new DefaultMessageContextFactory(new StaticMessageSource()));
MockExternalContext context = new MockExternalContext();
execution.start(null, context);
assertTrue(execution.isActive());
assertEquals(1, mockListener.getPausedCount());
}
public void testStartExceptionThrownBeforeFirstSessionCreated() {
Flow flow = new Flow("flow");
new EndState(flow, "end");

View File

@@ -39,12 +39,12 @@ public class SearchFlowExecutionTests extends AbstractXmlFlowExecutionTests {
public void testStartFlow() {
ExternalContext context = new MockExternalContext();
startFlow(context);
startFlow(null, context);
assertCurrentStateEquals("enterCriteria");
}
public void testCriteriaSubmitSuccess() {
startFlow(new MockExternalContext());
startFlow(null, new MockExternalContext());
MockExternalContext context = new MockExternalContext();
context.putRequestParameter("firstName", "Keith");
context.putRequestParameter("lastName", "Donald");
@@ -55,7 +55,7 @@ public class SearchFlowExecutionTests extends AbstractXmlFlowExecutionTests {
}
public void testNewSearch() {
startFlow(new MockExternalContext());
startFlow(null, new MockExternalContext());
MockExternalContext context = new MockExternalContext();
context.putRequestParameter("firstName", "Keith");
context.putRequestParameter("lastName", "Donald");
@@ -70,7 +70,7 @@ public class SearchFlowExecutionTests extends AbstractXmlFlowExecutionTests {
}
public void testSelectValidResult() {
startFlow(new MockExternalContext());
startFlow(null, new MockExternalContext());
MockExternalContext context = new MockExternalContext();
context.putRequestParameter("firstName", "Keith");
context.putRequestParameter("lastName", "Donald");