From a201ad2b38d68bc75b78f68b40f99fe39414b41e Mon Sep 17 00:00:00 2001 From: Erwin Vervaet Date: Sat, 14 Jun 2008 12:03:11 +0000 Subject: [PATCH] FlowPhaseListener now maintains FacesMessages accross a flow execution redirect by temporarily storing them in flash scope (SWF-745). FlowPhaseListener will now save the JSF component state before a flow execution redirect is issued (SWF-747). WIP - needs to be reviewed by Jeremy. --- spring-webflow/changelog.txt | 3 + .../executor/jsf/FlowPhaseListener.java | 90 ++++++++++++++++++- 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/spring-webflow/changelog.txt b/spring-webflow/changelog.txt index 9e5ec8dd..0b95f44c 100644 --- a/spring-webflow/changelog.txt +++ b/spring-webflow/changelog.txt @@ -17,6 +17,9 @@ Package org.springframework.webflow.executor * Added AutowiringFlowExecutionListener (SWF-746). * In a JSF 1.2 environment, SWF will now correctly maintain the "java.faces.ViewState" parameter accross flow execution redirects (SWF-445). +* FlowPhaseListener now maintains FacesMessages accross a flow execution redirect by temporarily storing them in + flash scope (SWF-745). +* FlowPhaseListener will now save the JSF component state before a flow execution redirect is issued (SWF-747). Changes in version 1.0.5 (03.10.2007) ------------------------------------- diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowPhaseListener.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowPhaseListener.java index 6881a401..dd8899d8 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowPhaseListener.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowPhaseListener.java @@ -16,9 +16,13 @@ package org.springframework.webflow.executor.jsf; import java.io.IOException; +import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; import java.util.Map; +import javax.faces.application.FacesMessage; import javax.faces.application.ViewHandler; import javax.faces.component.UIViewRoot; import javax.faces.context.FacesContext; @@ -49,7 +53,6 @@ import org.springframework.webflow.execution.support.FlowExecutionRedirect; import org.springframework.webflow.executor.RequestParameterInputMapper; import org.springframework.webflow.executor.ResponseInstruction; import org.springframework.webflow.executor.support.FlowExecutorArgumentHandler; -import org.springframework.webflow.executor.support.RequestParameterFlowExecutorArgumentHandler; import org.springframework.webflow.executor.support.ResponseInstructionHandler; /** @@ -117,7 +120,7 @@ public class FlowPhaseListener implements PhaseListener { * * How arguments are extracted and how URLs are generated can be customized by setting a custom argument handler. */ - private FlowExecutorArgumentHandler argumentHandler = new RequestParameterFlowExecutorArgumentHandler(); + private FlowExecutorArgumentHandler argumentHandler = new JsfArgumentHandler(); /** * The service responsible for mapping attributes of an {@link ExternalContext} to a new {@link FlowExecution} @@ -227,8 +230,9 @@ public class FlowPhaseListener implements PhaseListener { // we do not need to worry about clean up here since other phases will continue to run even if an exception // occurs in restoreFlowExecution(FacesContext) } else if (event.getPhaseId() == PhaseId.RENDER_RESPONSE) { - if (FlowExecutionHolderUtils.isFlowExecutionRestored(event.getFacesContext())) { + if (FlowExecutionHolderUtils.isFlowExecutionRestored(context)) { try { + restoreFacesMessages(context); prepareResponse(getCurrentContext(), FlowExecutionHolderUtils.getFlowExecutionHolder(context)); } catch (RuntimeException e) { // we must cleanup here since this is the render response phase and the after phase callback will @@ -248,6 +252,7 @@ public class FlowPhaseListener implements PhaseListener { if (event.getPhaseId() == PhaseId.RENDER_RESPONSE) { if (FlowExecutionHolderUtils.isFlowExecutionRestored(context)) { try { + saveFacesMessages(context); saveFlowExecution(getCurrentContext(), FlowExecutionHolderUtils.getFlowExecutionHolder(context)); } finally { // always cleanup after save - we are done with flow execution request processing @@ -374,6 +379,12 @@ public class FlowPhaseListener implements PhaseListener { protected void handleFlowExecutionRedirect(FlowExecutionRedirect redirect) throws Exception { String url = argumentHandler.createFlowExecutionUrl(holder.getFlowExecutionKey().toString(), holder .getFlowExecution(), context); + + // even though we are going to send a redirect, we still need to make sure the + // view state is preserved accross the redirect since we're not changing views + // (this is a flow execution redirect after all)! + context.getFacesContext().getViewRoot().processSaveState(context.getFacesContext()); + sendRedirect(url, context.getFacesContext()); } @@ -466,6 +477,8 @@ public class FlowPhaseListener implements PhaseListener { /** * Cleans up any allocated flow system resources and clears out the external context holder. * @param context the faces context + * + * @since 1.0.6 */ protected void cleanupResources(FacesContext context) { if (logger.isDebugEnabled()) { @@ -475,6 +488,69 @@ public class FlowPhaseListener implements PhaseListener { ExternalContextHolder.setExternalContext(null); } + /** + * Restore faces messages found inside the flow execution and add them back to given faces context. + * + * @since 1.0.6 + */ + protected void restoreFacesMessages(FacesContext context) { + MutableAttributeMap scope = getScope(context); + Map facesMessagesMap = (Map) scope.get(getFacesMessagesKey()); + if (facesMessagesMap != null) { + // restore messages by adding them back to the faces context + for (Iterator clientIds = facesMessagesMap.keySet().iterator(); clientIds.hasNext();) { + String clientId = (String) clientIds.next(); + for (Iterator messages = ((List) facesMessagesMap.get(clientId)).iterator(); messages.hasNext();) { + context.addMessage(clientId, (FacesMessage) messages.next()); + } + } + + // remove the restored messages from the flow execution so they cannot be restored again + scope.remove(getFacesMessagesKey()); + } + } + + /** + * Save faces messages in the flow execution, allowing them to survive a client side redirect. + * + * @since 1.0.6 + */ + protected void saveFacesMessages(FacesContext context) { + // gather all message to put away in the flow execution + Map facesMessagesMap = new HashMap(); + for (Iterator clientIds = context.getClientIdsWithMessages(); clientIds.hasNext();) { + String clientId = (String) clientIds.next(); + addFacesMessages(context, clientId, facesMessagesMap); + } + addFacesMessages(context, null, facesMessagesMap); + + // put them in a flow execution scope + MutableAttributeMap scope = getScope(context); + if (facesMessagesMap.isEmpty()) { + scope.remove(getFacesMessagesKey()); + } else { + scope.put(getFacesMessagesKey(), facesMessagesMap); + } + } + + /** + * Returns the scope map to store faces messages in. By default, flash scope is used. + * + * @since 1.0.6 + */ + protected MutableAttributeMap getScope(FacesContext context) { + return FlowExecutionHolderUtils.getCurrentFlowExecution(context).getActiveSession().getFlashMap(); + } + + /** + * Returns the key used to store the faces messages in one of the flow execution scopes. + * + * @since 1.0.6 + */ + protected String getFacesMessagesKey() { + return this.getClass().getName() + ".FacesMessages"; + } + // private helpers private void generateKey(JsfExternalContext context, FlowExecutionHolder holder) { @@ -549,6 +625,14 @@ public class FlowPhaseListener implements PhaseListener { } } + private void addFacesMessages(FacesContext context, String clientId, Map facesMessagesMap) { + List facesMessages = new LinkedList(); + for (Iterator messagesForClientId = context.getMessages(clientId); messagesForClientId.hasNext();) { + facesMessages.add(messagesForClientId.next()); + } + facesMessagesMap.put(clientId, facesMessages); + } + private JsfExternalContext getCurrentContext() { return (JsfExternalContext) ExternalContextHolder.getExternalContext(); }