diff --git a/build-spring-webflow/resources/changelog.txt b/build-spring-webflow/resources/changelog.txt index cc46401b..5e41e688 100644 --- a/build-spring-webflow/resources/changelog.txt +++ b/build-spring-webflow/resources/changelog.txt @@ -9,6 +9,7 @@ Modify Jsf2FlowFacesContext.isValidationFailed() to check Web Flow's MessageCont Recognize class-level bean validation messages in BindingResult.getGlobalErrors() Fix "embedded" mode to be flow session local, i.e. specific to a specific flow or subflow. Allow JSF view root to survive redirect in same state (following fix in JSF Mojarra 2.1) +Ensure PostRestoreStateEvent is delivered to registered listeners. Changes in version 2.3.0.RELEASE (Feb 28, 2011) ----------------------------------------------- diff --git a/spring-faces/src/main/java/org/springframework/faces/webflow/Jsf2FlowApplication.java b/spring-faces/src/main/java/org/springframework/faces/webflow/Jsf2FlowApplication.java index 0fd91129..12f5048c 100644 --- a/spring-faces/src/main/java/org/springframework/faces/webflow/Jsf2FlowApplication.java +++ b/spring-faces/src/main/java/org/springframework/faces/webflow/Jsf2FlowApplication.java @@ -26,7 +26,11 @@ import javax.faces.application.Resource; import javax.faces.application.ResourceHandler; import javax.faces.component.UIComponent; import javax.faces.component.behavior.Behavior; +import javax.faces.component.visit.VisitContext; import javax.faces.context.FacesContext; +import javax.faces.event.AbortProcessingException; +import javax.faces.event.ExceptionQueuedEvent; +import javax.faces.event.ExceptionQueuedEventContext; import javax.faces.event.SystemEvent; import javax.faces.event.SystemEventListener; @@ -116,4 +120,17 @@ public class Jsf2FlowApplication extends FlowApplication { getDelegate().unsubscribeFromEvent(systemEventClass, listener); } + // Ideally this method should be in JsfView + // We keep it here to avoid ClassNotFoundExceptions for JSF 1.2 apps + + static void publishPostRestoreStateEvent() { + FacesContext facesContext = FlowFacesContext.getCurrentInstance(); + try { + facesContext.getViewRoot().visitTree(VisitContext.createVisitContext(facesContext), + new PostRestoreStateEventVisitCallback()); + } catch (AbortProcessingException e) { + facesContext.getApplication().publishEvent(facesContext, ExceptionQueuedEvent.class, + new ExceptionQueuedEventContext(facesContext, e, null, facesContext.getCurrentPhaseId())); + } + } } diff --git a/spring-faces/src/main/java/org/springframework/faces/webflow/JsfUtils.java b/spring-faces/src/main/java/org/springframework/faces/webflow/JsfUtils.java index 94bff65e..14a87676 100644 --- a/spring-faces/src/main/java/org/springframework/faces/webflow/JsfUtils.java +++ b/spring-faces/src/main/java/org/springframework/faces/webflow/JsfUtils.java @@ -15,12 +15,15 @@ */ package org.springframework.faces.webflow; +import java.lang.reflect.Method; + import javax.faces.context.FacesContext; import javax.faces.event.PhaseEvent; import javax.faces.event.PhaseId; import javax.faces.event.PhaseListener; import javax.faces.lifecycle.Lifecycle; +import org.springframework.util.ReflectionUtils; import org.springframework.webflow.execution.RequestContextHolder; /** @@ -66,4 +69,18 @@ public class JsfUtils { } } + // This method is here for JSF 1.2 backwards compatibility + + static void publishPostRestoreStateEvent() { + try { + Class clazz = Class.forName("org.springframework.faces.webflow.Jsf2FlowApplication"); + Method method = ReflectionUtils.findMethod(clazz, "publishPostRestoreStateEvent"); + ReflectionUtils.makeAccessible(method); + ReflectionUtils.invokeMethod(method, null); + + } catch (ClassNotFoundException ex) { + throw new IllegalStateException("Expected Jsf2FlowApplication: " + ex); + } + } + } diff --git a/spring-faces/src/main/java/org/springframework/faces/webflow/JsfViewFactory.java b/spring-faces/src/main/java/org/springframework/faces/webflow/JsfViewFactory.java index a5186356..5cfa83c9 100644 --- a/spring-faces/src/main/java/org/springframework/faces/webflow/JsfViewFactory.java +++ b/spring-faces/src/main/java/org/springframework/faces/webflow/JsfViewFactory.java @@ -128,6 +128,9 @@ public class JsfViewFactory implements ViewFactory { view = createJsfView(viewRoot, lifecycle, context); } } + if (isAtLeastJsf20()) { + JsfUtils.publishPostRestoreStateEvent(); + } if (!facesContext.getRenderResponse()) { JsfUtils.notifyAfterListeners(PhaseId.RESTORE_VIEW, lifecycle, facesContext); } @@ -182,4 +185,5 @@ public class JsfViewFactory implements ViewFactory { processTree(context, child); } } + } diff --git a/spring-faces/src/main/java/org/springframework/faces/webflow/PostRestoreStateEventVisitCallback.java b/spring-faces/src/main/java/org/springframework/faces/webflow/PostRestoreStateEventVisitCallback.java new file mode 100644 index 00000000..8d4009ee --- /dev/null +++ b/spring-faces/src/main/java/org/springframework/faces/webflow/PostRestoreStateEventVisitCallback.java @@ -0,0 +1,28 @@ +package org.springframework.faces.webflow; + +import javax.faces.component.UIComponent; +import javax.faces.component.visit.VisitCallback; +import javax.faces.component.visit.VisitContext; +import javax.faces.component.visit.VisitResult; +import javax.faces.event.PostRestoreStateEvent; + +/** + * A VisitCallback used to deliver a PostRestoreStataEvent similar to + * {@code RestoreViewPhase.deliverPostRestoreStateEvent(..)} in Sun's JSF. + * + * @since 2.3.1 + */ +class PostRestoreStateEventVisitCallback implements VisitCallback { + + private PostRestoreStateEvent event; + + public VisitResult visit(VisitContext context, UIComponent target) { + if (this.event == null) { + this.event = new PostRestoreStateEvent(target); + } else { + this.event.setComponent(target); + } + target.processEvent(event); + return VisitResult.ACCEPT; + } +} diff --git a/spring-faces/src/test/java/org/springframework/faces/webflow/JSFMockHelper.java b/spring-faces/src/test/java/org/springframework/faces/webflow/JSFMockHelper.java index 8e9423ce..e80a1741 100644 --- a/spring-faces/src/test/java/org/springframework/faces/webflow/JSFMockHelper.java +++ b/spring-faces/src/test/java/org/springframework/faces/webflow/JSFMockHelper.java @@ -28,13 +28,14 @@ import org.apache.myfaces.test.mock.MockServletConfig; import org.apache.myfaces.test.mock.MockServletContext; import org.apache.myfaces.test.mock.lifecycle.MockLifecycle; import org.apache.myfaces.test.mock.lifecycle.MockLifecycleFactory; +import org.apache.myfaces.test.mock.visit.MockVisitContextFactory; /** * Helper for using the mock JSF environment provided by shale-test inside unit tests that do not extend * {@link AbstractJsfTestCase} * * @author Jeremy Grelle - * @author Phil Webb + * @author Phillip Webb */ public class JSFMockHelper { @@ -137,7 +138,9 @@ public class JSFMockHelper { FactoryFinder.setFactory(FactoryFinder.FACES_CONTEXT_FACTORY, MockBaseFacesContextFactory.class.getName()); FactoryFinder.setFactory(FactoryFinder.LIFECYCLE_FACTORY, MockLifecycleFactory.class.getName()); FactoryFinder.setFactory(FactoryFinder.RENDER_KIT_FACTORY, MockRenderKitFactory.class.getName()); - FactoryFinder.setFactory(FactoryFinder.PARTIAL_VIEW_CONTEXT_FACTORY, MockPartialViewContextFactory.class.getName()); + FactoryFinder.setFactory(FactoryFinder.PARTIAL_VIEW_CONTEXT_FACTORY, + MockPartialViewContextFactory.class.getName()); + FactoryFinder.setFactory(FactoryFinder.VISIT_CONTEXT_FACTORY, MockVisitContextFactory.class.getName()); lifecycleFactory = (MockLifecycleFactory) FactoryFinder.getFactory(FactoryFinder.LIFECYCLE_FACTORY); lifecycle = (MockLifecycle) lifecycleFactory.getLifecycle(LifecycleFactory.DEFAULT_LIFECYCLE); facesContextFactory = (FacesContextFactory) FactoryFinder.getFactory(FactoryFinder.FACES_CONTEXT_FACTORY); diff --git a/spring-faces/src/test/java/org/springframework/faces/webflow/JsfViewFactoryTests.java b/spring-faces/src/test/java/org/springframework/faces/webflow/JsfViewFactoryTests.java index fb3c9b4e..1e8a4c36 100644 --- a/spring-faces/src/test/java/org/springframework/faces/webflow/JsfViewFactoryTests.java +++ b/spring-faces/src/test/java/org/springframework/faces/webflow/JsfViewFactoryTests.java @@ -11,13 +11,20 @@ import javax.faces.component.UIOutput; import javax.faces.component.UIPanel; import javax.faces.component.UIViewRoot; import javax.faces.context.FacesContext; +import javax.faces.event.AbortProcessingException; +import javax.faces.event.ComponentSystemEvent; +import javax.faces.event.ExceptionQueuedEvent; +import javax.faces.event.ExceptionQueuedEventContext; import javax.faces.event.PhaseEvent; import javax.faces.event.PhaseId; import javax.faces.event.PhaseListener; +import javax.faces.event.PostRestoreStateEvent; +import javax.faces.event.SystemEvent; import javax.faces.lifecycle.Lifecycle; import junit.framework.TestCase; +import org.apache.myfaces.test.mock.MockApplication20; import org.easymock.EasyMock; import org.jboss.el.ExpressionFactoryImpl; import org.springframework.binding.expression.ExpressionParser; @@ -82,6 +89,8 @@ public class JsfViewFactoryTests extends TestCase { private void configureJsf() throws Exception { jsfMock.setUp(); + ExceptionEventAwareMockApplication application = new ExceptionEventAwareMockApplication(); + ((MockBaseFacesContext) FlowFacesContext.getCurrentInstance()).setApplication(application); trackingListener = new TrackingPhaseListener(); jsfMock.lifecycle().addPhaseListener(trackingListener); jsfMock.facesContext().setViewRoot(null); @@ -98,7 +107,7 @@ public class JsfViewFactoryTests extends TestCase { new FluentParserContext().template().evaluate(RequestContext.class).expectResult(String.class)), lifecycle); - UIViewRoot newRoot = new UIViewRoot(); + MockUIViewRoot newRoot = new MockUIViewRoot(); newRoot.setViewId(VIEW_ID); ((MockViewHandler) viewHandler).setCreateView(newRoot); context.inViewState(); @@ -115,7 +124,7 @@ public class JsfViewFactoryTests extends TestCase { } /** - * View already exists in view/flash scope and must be restored and the lifecycle executed, no event signaled + * View already exists in view/flash scope and must be restored and the lifecycle executed, no flow event signaled */ public final void testGetView_Restore() { @@ -124,7 +133,7 @@ public class JsfViewFactoryTests extends TestCase { new FluentParserContext().template().evaluate(RequestContext.class).expectResult(String.class)), lifecycle); - UIViewRoot existingRoot = new UIViewRoot(); + MockUIViewRoot existingRoot = new MockUIViewRoot(); existingRoot.setViewId(VIEW_ID); UIInput input = new UIInput(); input.setId("invalidInput"); @@ -144,10 +153,11 @@ public class JsfViewFactoryTests extends TestCase { assertEquals("View name did not match", VIEW_ID, ((JsfView) restoredView).getViewRoot().getViewId()); assertFalse("An unexpected event was signaled,", restoredView.hasFlowEvent()); assertTrue("The input component's valid flag was not reset", input.isValid()); + assertTrue("The PostRestoreViewEvent was not seen", existingRoot.isPostRestoreStateEventSeen()); } /** - * View already exists in view/flash scope and must be restored and the lifecycle executed, no event signaled + * View already exists in view/flash scope and must be restored and the lifecycle executed, no flow event signaled */ public final void testGetView_RestoreWithBindings() { @@ -156,7 +166,7 @@ public class JsfViewFactoryTests extends TestCase { new FluentParserContext().template().evaluate(RequestContext.class).expectResult(String.class)), lifecycle); - UIViewRoot existingRoot = new UIViewRoot(); + MockUIViewRoot existingRoot = new MockUIViewRoot(); existingRoot.setViewId(VIEW_ID); UIPanel panel = new UIPanel(); panel.setId("panel1"); @@ -190,11 +200,12 @@ public class JsfViewFactoryTests extends TestCase { assertFalse("An unexpected event was signaled,", restoredView.hasFlowEvent()); assertSame("The UIInput binding was not restored properly", input, testBean.getInput()); assertSame("The faceted UIOutput binding was not restored properly", output, testBean.getOutput()); + assertTrue("The PostRestoreViewEvent was not seen", existingRoot.isPostRestoreStateEventSeen()); } /** - * Ajax Request - View already exists in view/flash scope and must be restored and the lifecycle executed, no event - * signaled + * Ajax Request - View already exists in view/flash scope and must be restored and the lifecycle executed, no flow + * event signaled */ public final void testGetView_Restore_Ajax() { @@ -203,7 +214,7 @@ public class JsfViewFactoryTests extends TestCase { new FluentParserContext().template().evaluate(RequestContext.class).expectResult(String.class)), lifecycle); - UIViewRoot existingRoot = new UIViewRoot(); + MockUIViewRoot existingRoot = new MockUIViewRoot(); existingRoot.setViewId(VIEW_ID); ((MockViewHandler) viewHandler).setRestoreView(existingRoot); @@ -223,6 +234,7 @@ public class JsfViewFactoryTests extends TestCase { assertTrue("An ViewRoot was not set", ((JsfView) restoredView).getViewRoot() instanceof UIViewRoot); assertEquals("View name did not match", VIEW_ID, ((JsfView) restoredView).getViewRoot().getViewId()); assertFalse("An unexpected event was signaled,", restoredView.hasFlowEvent()); + assertTrue("The PostRestoreViewEvent was not seen", existingRoot.isPostRestoreStateEventSeen()); } /** @@ -234,7 +246,7 @@ public class JsfViewFactoryTests extends TestCase { new FluentParserContext().template().evaluate(RequestContext.class).expectResult(String.class)), lifecycle); - UIViewRoot newRoot = new UIViewRoot(); + MockUIViewRoot newRoot = new MockUIViewRoot(); newRoot.setViewId(VIEW_ID); jsfMock.facesContext().setViewRoot(newRoot); jsfMock.facesContext().renderResponse(); @@ -248,6 +260,30 @@ public class JsfViewFactoryTests extends TestCase { assertEquals("View name did not match", VIEW_ID, ((JsfView) newView).getViewRoot().getViewId()); assertSame("View root was not the third party instance", newRoot, ((JsfView) newView).getViewRoot()); assertFalse("An unexpected event was signaled,", newView.hasFlowEvent()); + assertTrue("The PostRestoreViewEvent was not seen", newRoot.isPostRestoreStateEventSeen()); + } + + public void testGetView_ExceptionsOnPostRestoreStateEvent() throws Exception { + lifecycle = new NoExecutionLifecycle(jsfMock.lifecycle()); + factory = new JsfViewFactory(parser.parseExpression(VIEW_ID, + new FluentParserContext().template().evaluate(RequestContext.class).expectResult(String.class)), + lifecycle); + + MockUIViewRoot existingRoot = new MockUIViewRoot(); + existingRoot.setThrowOnPostRestoreStateEvent(true); + existingRoot.setViewId(VIEW_ID); + ((MockViewHandler) viewHandler).setRestoreView(existingRoot); + + context.inViewState(); + EasyMock.expectLastCall().andReturn(Boolean.TRUE); + + EasyMock.replay(new Object[] { context }); + factory.getView(context); + ExceptionEventAwareMockApplication application = (ExceptionEventAwareMockApplication) FlowFacesContext + .getCurrentInstance().getApplication(); + assertNotNull("Expected exception event", application.getExceptionQueuedEventContext()); + assertSame("Expected same exception", existingRoot.getAbortProcessingException(), application + .getExceptionQueuedEventContext().getException()); } private class NoExecutionLifecycle extends FlowLifecycle { @@ -337,4 +373,51 @@ public class JsfViewFactoryTests extends TestCase { this.input = input; } } + + private static class MockUIViewRoot extends UIViewRoot { + + private boolean postRestoreStateEventSeen; + private boolean throwOnPostRestoreStateEvent; + private AbortProcessingException abortProcessingException; + + public void processEvent(ComponentSystemEvent event) throws AbortProcessingException { + if (event instanceof PostRestoreStateEvent) { + assertSame("Component did not match", this, ((PostRestoreStateEvent) event).getComponent()); + postRestoreStateEventSeen = true; + if (throwOnPostRestoreStateEvent) { + abortProcessingException = new AbortProcessingException(); + throw abortProcessingException; + } + } + } + + public void setThrowOnPostRestoreStateEvent(boolean throwOnPostRestoreStateEvent) { + this.throwOnPostRestoreStateEvent = throwOnPostRestoreStateEvent; + } + + public boolean isPostRestoreStateEventSeen() { + return postRestoreStateEventSeen; + } + + public AbortProcessingException getAbortProcessingException() { + return abortProcessingException; + } + } + + private static class ExceptionEventAwareMockApplication extends MockApplication20 { + + private ExceptionQueuedEventContext exceptionQueuedEventContext; + + public void publishEvent(FacesContext facesContext, Class systemEventClass, Object source) { + if (ExceptionQueuedEvent.class.equals(systemEventClass)) { + this.exceptionQueuedEventContext = (ExceptionQueuedEventContext) source; + } else { + super.publishEvent(facesContext, systemEventClass, source); + } + } + + public ExceptionQueuedEventContext getExceptionQueuedEventContext() { + return exceptionQueuedEventContext; + } + } } diff --git a/spring-faces/src/test/java/org/springframework/faces/webflow/MockBaseFacesContext.java b/spring-faces/src/test/java/org/springframework/faces/webflow/MockBaseFacesContext.java index 7b4fbe08..1a891a0b 100644 --- a/spring-faces/src/test/java/org/springframework/faces/webflow/MockBaseFacesContext.java +++ b/spring-faces/src/test/java/org/springframework/faces/webflow/MockBaseFacesContext.java @@ -38,4 +38,8 @@ public class MockBaseFacesContext extends MockFacesContext20 { } return application; } + + public void setApplication(Application application) { + this.application = application; + } }