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

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