Add support for PostRestoreStateEvent

A PostRestoreStateEvent should be delivered whenever a JSF view is
restored. Typically we rely on JSF to generate required events but
in this case since WebFlow handles the RESTORE_VIEW phase itself,
we need to deliver this event ourselves.

Issues: SWF-1500
This commit is contained in:
Phil Webb
2012-03-03 12:05:13 -08:00
committed by Rossen Stoyanchev
parent 8e1aa91b40
commit 27cd5cca31
8 changed files with 168 additions and 11 deletions

View File

@@ -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)
-----------------------------------------------

View File

@@ -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()));
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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<? extends SystemEvent> systemEventClass, Object source) {
if (ExceptionQueuedEvent.class.equals(systemEventClass)) {
this.exceptionQueuedEventContext = (ExceptionQueuedEventContext) source;
} else {
super.publishEvent(facesContext, systemEventClass, source);
}
}
public ExceptionQueuedEventContext getExceptionQueuedEventContext() {
return exceptionQueuedEventContext;
}
}
}

View File

@@ -38,4 +38,8 @@ public class MockBaseFacesContext extends MockFacesContext20 {
}
return application;
}
public void setApplication(Application application) {
this.application = application;
}
}