diff --git a/spring-faces/src/main/java/org/springframework/faces/webflow/JsfView.java b/spring-faces/src/main/java/org/springframework/faces/webflow/JsfView.java index c796fa99..ebe6c3a2 100644 --- a/spring-faces/src/main/java/org/springframework/faces/webflow/JsfView.java +++ b/spring-faces/src/main/java/org/springframework/faces/webflow/JsfView.java @@ -73,7 +73,7 @@ public class JsfView implements View { /** * This implementation performs the standard duties of the JSF RENDER_RESPONSE phase. */ - public void render() { + public void render() throws IOException { FacesContext facesContext = createFlowFacesContext(); facesContext.setViewRoot(viewRoot); facesContext.renderResponse(); diff --git a/spring-faces/src/test/java/org/springframework/faces/webflow/JsfViewTests.java b/spring-faces/src/test/java/org/springframework/faces/webflow/JsfViewTests.java index 3dd1e032..2e0aef4f 100644 --- a/spring-faces/src/test/java/org/springframework/faces/webflow/JsfViewTests.java +++ b/spring-faces/src/test/java/org/springframework/faces/webflow/JsfViewTests.java @@ -81,7 +81,7 @@ public class JsfViewTests extends TestCase { jsfMock.tearDown(); } - public final void testRender() { + public final void testRender() throws IOException { EasyMock.expect(requestContext.getExternalContext()).andStubReturn(new MockExternalContext()); EasyMock.expect(requestContext.getFlashScope()).andStubReturn(flashMap); @@ -99,7 +99,7 @@ public class JsfViewTests extends TestCase { assertNull("The FacesContext was not released", FacesContext.getCurrentInstance()); } - public final void testRenderException() { + public final void testRenderException() throws IOException { EasyMock.expect(requestContext.getExternalContext()).andStubReturn(new MockExternalContext()); EasyMock.expect(requestContext.getFlashScope()).andStubReturn(flashMap); diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/ViewRenderingException.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/ViewRenderingException.java new file mode 100644 index 00000000..31bf639f --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/ViewRenderingException.java @@ -0,0 +1,24 @@ +package org.springframework.webflow.engine; + +import org.springframework.webflow.execution.FlowExecutionException; +import org.springframework.webflow.execution.View; + +/** + * Thrown if a IO exception was thrown during view rendering. + * + * @author Keith Donald + */ +public class ViewRenderingException extends FlowExecutionException { + + /** + * Create a new action execution exception. + * @param flowId the current flow + * @param stateId the current state (may be null) + * @param view the view that generated an unrecoverable exception + * @param cause the underlying cause + */ + public ViewRenderingException(String flowId, String stateId, View view, Throwable cause) { + super(flowId, stateId, "Exception thrown rendering " + view + " in state '" + stateId + "' of flow '" + flowId + + "'", cause); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/ViewState.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/ViewState.java index c6bc4e78..79798bd8 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/ViewState.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/ViewState.java @@ -15,6 +15,7 @@ */ package org.springframework.webflow.engine; +import java.io.IOException; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; @@ -214,14 +215,18 @@ public class ViewState extends TransitionableState { } } - private void render(RequestControlContext context, View view) { + private void render(RequestControlContext context, View view) throws ViewRenderingException { if (logger.isDebugEnabled()) { logger.debug("Rendering + " + view); logger.debug(" Flash scope = " + context.getFlashScope()); logger.debug(" Messages = " + context.getMessageContext()); } renderActionList.execute(context); - view.render(); + try { + view.render(); + } catch (IOException e) { + throw new ViewRenderingException(getOwner().getId(), getId(), view, e); + } context.getMessageContext().clearMessages(); context.getFlashScope().clear(); } diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/View.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/View.java index f150e8b5..080efb35 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/execution/View.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/View.java @@ -15,6 +15,8 @@ */ package org.springframework.webflow.execution; +import java.io.IOException; + /** * Allows the client to participate in flow execution. Encapsulates behavior to send the client an appropriate response * and handle the resulting event once the client responds. @@ -31,8 +33,9 @@ public interface View { /** * Render this view's content. + * @throws IOException if an IO Exception occured rendering the view */ - public void render(); + public void render() throws IOException; /** * Was a user event signaled on this view in this request? diff --git a/spring-webflow/src/main/java/org/springframework/webflow/test/MockExternalContext.java b/spring-webflow/src/main/java/org/springframework/webflow/test/MockExternalContext.java index 142aeea4..e94f3168 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/test/MockExternalContext.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/test/MockExternalContext.java @@ -256,6 +256,15 @@ public class MockExternalContext implements ExternalContext { getMockRequestParameterMap().put(parameterName, parameterValues); } + /** + * Sets the id of the event that should be signaled by this context. For use when resuming a flow. This method + * depends on a MockViewFactory being configured for parsing the event id on a resume operation. + * @param eventId the id of the event to signal + */ + public void setEventId(String eventId) { + putRequestParameter("_eventId", eventId); + } + /** * Set whether this request is an ajax request. * @param ajaxRequest true or false @@ -264,6 +273,14 @@ public class MockExternalContext implements ExternalContext { this.ajaxRequest = ajaxRequest; } + /** + * Returns the implementation of this mock context's response writer. + * @return the underlying string writer to use for asserting a specific response was written + */ + public StringWriter getMockResponseWriter() { + return responseWriter; + } + /** * Returns the flag indicating if a flow execution redirect response has been requested by the flow. */ diff --git a/spring-webflow/src/main/java/org/springframework/webflow/test/MockViewFactoryCreator.java b/spring-webflow/src/main/java/org/springframework/webflow/test/MockViewFactoryCreator.java index aeff28ac..afd1f0aa 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/test/MockViewFactoryCreator.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/test/MockViewFactoryCreator.java @@ -15,6 +15,8 @@ */ package org.springframework.webflow.test; +import java.io.IOException; + import org.springframework.binding.expression.Expression; import org.springframework.core.io.ResourceLoader; import org.springframework.webflow.action.ViewFactoryActionAdapter; @@ -102,8 +104,8 @@ class MockViewFactoryCreator implements ViewFactoryCreator { return new Event(this, context.getRequestParameters().get("_eventId")); } - public void render() { - // nothing to do + public void render() throws IOException { + context.getExternalContext().getResponseWriter().append(viewId); } } } \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/test/execution/AbstractFlowExecutionTests.java b/spring-webflow/src/main/java/org/springframework/webflow/test/execution/AbstractFlowExecutionTests.java index 464a7b4d..0d13e049 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/test/execution/AbstractFlowExecutionTests.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/test/execution/AbstractFlowExecutionTests.java @@ -25,7 +25,6 @@ import org.springframework.webflow.execution.FlowExecution; import org.springframework.webflow.execution.FlowExecutionException; import org.springframework.webflow.execution.FlowExecutionFactory; import org.springframework.webflow.test.MockExternalContext; -import org.springframework.webflow.test.MockParameterMap; /** * Base class for integration tests that verify a flow executes as expected. Flow execution tests captured by subclasses @@ -119,45 +118,6 @@ public abstract class AbstractFlowExecutionTests extends TestCase { flowExecution.resume(context); } - /** - * Signal the event against the paused flow execution. The event id will be translated into a Event object by the - * configured view factory and raised as an Event against the current view state. The event will cause the flow to - * change states if it matches a transition. - * @param eventId the event identifier - */ - protected void signalEvent(String eventId) { - MockExternalContext context = new MockExternalContext(); - context.putRequestParameter("_eventId", eventId); - resumeFlow(context); - } - - /** - * Signal the event against the paused flow execution. The event id will be translated into a Event object by the - * configured view factory and raised as an Event against the current view state. The event will cause the flow to - * change states if it matches a transition. - * @param eventId the event identifier - * @param input event input parameters - */ - protected void signalEvent(String eventId, MockParameterMap input) { - MockExternalContext context = new MockExternalContext(input); - context.putRequestParameter("_eventId", eventId); - resumeFlow(context); - } - - /** - * Signal the event against the paused flow execution. The event id will be translated into a Event object by the - * configured view factory and raised as an Event against the current view state. The event will cause the flow to - * change states if it matches a transition. - * @param eventId the event identifier - * @param parameterName the name of the parameter - * @param parameterValue the value of the parameter - */ - protected void signalEvent(String eventId, String parameterName, String parameterValue) { - MockParameterMap input = new MockParameterMap(); - input.put(parameterName, parameterValue); - signalEvent(eventId, input); - } - // convenience accessors /** @@ -268,6 +228,13 @@ public abstract class AbstractFlowExecutionTests extends TestCase { // assert helpers + /** + * Assert that the entire flow execution is active; that is, it has not ended and has been started. + */ + protected void assertFlowExecutionActive() { + assertTrue("The flow execution is not active but it should be", getFlowExecution().isActive()); + } + /** * Assert that the active flow session is for the flow with the provided id. * @param expectedActiveFlowId the flow id that should have a session active in the tested flow execution @@ -278,13 +245,6 @@ public abstract class AbstractFlowExecutionTests extends TestCase { getFlowExecution().getActiveSession().getDefinition().getId()); } - /** - * Assert that the entire flow execution is active; that is, it has not ended and has been started. - */ - protected void assertFlowExecutionActive() { - assertTrue("The flow execution is not active but it should be", getFlowExecution().isActive()); - } - /** * Assert that the entire flow execution has ended; that is, it is no longer active. */ @@ -303,6 +263,15 @@ public abstract class AbstractFlowExecutionTests extends TestCase { getFlowExecution().getActiveSession().getState().getId()); } + /** + * Assert that the response written to the mock context equals the response provided. + * @param response the expected response + * @param context the mock external context that was written to + */ + protected void assertResponseWrittenEquals(String response, MockExternalContext context) { + assertEquals(response, context.getMockResponseWriter().getBuffer().toString()); + } + /** * Factory method to create the flow execution factory. Subclasses could override this if they want to use a custom * flow execution factory or custom configuration of the flow execution factory, registering flow execution diff --git a/spring-webflow/src/test/java/org/springframework/webflow/mvc/MvcViewFactoryTests.java b/spring-webflow/src/test/java/org/springframework/webflow/mvc/MvcViewFactoryTests.java index 66a72d69..c334b56e 100644 --- a/spring-webflow/src/test/java/org/springframework/webflow/mvc/MvcViewFactoryTests.java +++ b/spring-webflow/src/test/java/org/springframework/webflow/mvc/MvcViewFactoryTests.java @@ -38,7 +38,7 @@ public class MvcViewFactoryTests extends TestCase { context = new StaticApplicationContext(); } - public void testNoResolversGetResource() { + public void testNoResolversGetResource() throws Exception { creator.setApplicationContext(context); ResourceLoader viewResourceLoader = new ResourceLoader() { public ClassLoader getClassLoader() { @@ -65,7 +65,7 @@ public class MvcViewFactoryTests extends TestCase { assertEquals("/parent/myview.jsp", response.getForwardedUrl()); } - public void testViewResolversGetResource() { + public void testViewResolversGetResource() throws Exception { MockViewResolver viewResolver = new MockViewResolver("myview"); creator.setApplicationContext(context); creator.setViewResolvers(Collections.singletonList(viewResolver)); @@ -85,7 +85,7 @@ public class MvcViewFactoryTests extends TestCase { assertEquals("myview", response.getForwardedUrl()); } - public void testRestoreView() { + public void testRestoreView() throws Exception { creator.setApplicationContext(context); ResourceLoader viewResourceLoader = new ResourceLoader() { public ClassLoader getClassLoader() { @@ -116,7 +116,7 @@ public class MvcViewFactoryTests extends TestCase { assertEquals("/parent/myview.jsp", response.getForwardedUrl()); } - public void testRestoreViewButtonEventIdFormat() { + public void testRestoreViewButtonEventIdFormat() throws Exception { creator.setApplicationContext(context); ResourceLoader viewResourceLoader = new ResourceLoader() { public ClassLoader getClassLoader() { diff --git a/spring-webflow/src/test/java/org/springframework/webflow/test/SearchFlowExecutionTests.java b/spring-webflow/src/test/java/org/springframework/webflow/test/SearchFlowExecutionTests.java index f45b04f8..80a153eb 100644 --- a/spring-webflow/src/test/java/org/springframework/webflow/test/SearchFlowExecutionTests.java +++ b/spring-webflow/src/test/java/org/springframework/webflow/test/SearchFlowExecutionTests.java @@ -45,25 +45,44 @@ public class SearchFlowExecutionTests extends AbstractXmlFlowExecutionTests { public void testCriteriaSubmitSuccess() { startFlow(new MockExternalContext()); - MockParameterMap input = new MockParameterMap(); - input.put("firstName", "Keith"); - input.put("lastName", "Donald"); - signalEvent("search", input); + MockExternalContext context = new MockExternalContext(); + context.putRequestParameter("firstName", "Keith"); + context.putRequestParameter("lastName", "Donald"); + context.setEventId("search"); + resumeFlow(context); assertCurrentStateEquals("displayResults"); + assertResponseWrittenEquals("searchResults", context); } public void testNewSearch() { startFlow(new MockExternalContext()); - signalEvent("search"); - signalEvent("newSearch"); + MockExternalContext context = new MockExternalContext(); + context.putRequestParameter("firstName", "Keith"); + context.putRequestParameter("lastName", "Donald"); + context.setEventId("search"); + resumeFlow(context); + + context = new MockExternalContext(); + context.setEventId("newSearch"); + resumeFlow(context); assertCurrentStateEquals("enterCriteria"); + assertResponseWrittenEquals("searchCriteria", context); } public void testSelectValidResult() { startFlow(new MockExternalContext()); - signalEvent("search"); - signalEvent("select", "id", "1"); + MockExternalContext context = new MockExternalContext(); + context.putRequestParameter("firstName", "Keith"); + context.putRequestParameter("lastName", "Donald"); + context.setEventId("search"); + resumeFlow(context); + + context = new MockExternalContext(); + context.setEventId("select"); + context.putRequestParameter("id", "1"); + resumeFlow(context); assertCurrentStateEquals("displayResults"); + assertResponseWrittenEquals("searchResults", context); } protected void configureFlowBuilderContext(MockFlowBuilderContext builderContext) {