diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowExecutionKeyStateHolder.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowExecutionKeyStateHolder.java new file mode 100644 index 00000000..bfb53439 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowExecutionKeyStateHolder.java @@ -0,0 +1,147 @@ +package org.springframework.webflow.executor.jsf; +/* + * Copyright 2004-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import javax.faces.component.UIComponent; +import javax.faces.component.UIComponentBase; +import javax.faces.component.UIViewRoot; +import javax.faces.context.FacesContext; +import javax.faces.event.PhaseId; +import javax.faces.render.Renderer; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.util.StringUtils; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.repository.FlowExecutionKey; +import org.springframework.webflow.execution.repository.FlowExecutionLock; +import org.springframework.webflow.execution.repository.FlowExecutionRepository; + +/** + * This {@link UIComponent} instance can be added to the {@link UIViewRoot} before rendering so that the + * {@link FlowExecution} can be properly saved and then restored during the next request's {@link PhaseId#RESTORE_VIEW} + * phase. + * + * @author Jeremy Grelle + */ +public class FlowExecutionKeyStateHolder extends UIComponentBase { + + /** + * Logger, usable by subclasses. + */ + protected final Log logger = LogFactory.getLog(getClass()); + + private static final String COMPONENT_FAMILY = "javax.faces.Parameter"; + + /** + * Immutable id of the flow execution key component for easier lookup later. + */ + public static final String COMPONENT_ID = "FlowExecutionKeyStateHolder"; + + /** + * The key value + */ + private String flowExecutionKey; + + private boolean transientValue; + + public String getId() { + return COMPONENT_ID; + } + + public void setId(String id) { + // Do nothing so as to ensure the id never gets overwritten. + return; + } + + public String getFamily() { + return COMPONENT_FAMILY; + } + + public Renderer getRenderer() { + // this component is not rendered + return null; + } + + /** + * Returns the flow execution key. + */ + public String getFlowExecutionKey() { + return flowExecutionKey; + } + + /** + * Sets the tracked flow execution key used to restore the current flow execution during + * {@link #restoreState(FacesContext, Object)}. + * @param flowExecutionKey the flow execution key + */ + public void setFlowExecutionKey(String flowExecutionKey) { + this.flowExecutionKey = flowExecutionKey; + } + + public boolean isTransient() { + return transientValue; + } + + public void setTransient(boolean transientValue) { + this.transientValue = transientValue; + } + + /** + * Restore the FlowExecution from the stored FlowExecutionKey + */ + public void restoreState(FacesContext context, Object state) { + Object values[] = (Object[]) state; + flowExecutionKey = (String) values[0]; + restoreFlowExecution(context); + } + + private void restoreFlowExecution(FacesContext facesContext) { + JsfExternalContext context = new JsfExternalContext(facesContext); + if (StringUtils.hasText(flowExecutionKey)) { + // restore flow execution from repository so it will be available to variable/property resolvers + // and the flow navigation handler (this could happen as part of a submission or flow execution redirect) + FlowExecutionRepository repository = getRepository(context); + FlowExecutionKey key; + // restore the key from the stored flowExecutionKey + key = repository.parseFlowExecutionKey(flowExecutionKey); + FlowExecutionLock lock = repository.getLock(key); + lock.lock(); + FlowExecution flowExecution = repository.getFlowExecution(key); + if (logger.isDebugEnabled()) { + logger.debug("Loaded existing flow execution from repository with key '" + key + "'"); + } + FlowExecutionHolderUtils.setFlowExecutionHolder(new FlowExecutionHolder(key, flowExecution, lock), + facesContext); + } + } + + /** + * Save the just the current FlowExecutionKey value. + */ + public Object saveState(FacesContext context) { + Object values[] = new Object[1]; + values[0] = flowExecutionKey; + return values; + } + + public String getClientId(FacesContext context) { + return COMPONENT_ID; + } + + private FlowExecutionRepository getRepository(JsfExternalContext context) { + return FlowFacesUtils.getExecutionRepository(context.getFacesContext()); + } +} \ No newline at end of file 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 5c3fbaf8..dd9bd168 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 @@ -58,27 +58,23 @@ import org.springframework.webflow.executor.support.ResponseInstructionHandler; * This phase listener implements the following algorithm: * * * @author Colin Sampaleanu * @author Keith Donald + * @author Jeremy Grelle */ public class FlowPhaseListener implements PhaseListener { - /** - * The name of the attribute the flow execution key will be tracked under in the JSF View Root for managing the - * current flow execution key. - */ - private static final String FLOW_EXECUTION_KEY_VIEW_ROOT_ATTRIBUTE = "_flowExecutionKey"; - /** * Logger, usable by subclasses. */ @@ -157,7 +153,11 @@ public class FlowPhaseListener implements PhaseListener { } public void beforePhase(PhaseEvent event) { - if (event.getPhaseId() == PhaseId.RENDER_RESPONSE) { + if (event.getPhaseId() == PhaseId.RESTORE_VIEW) { + ExternalContextHolder.setExternalContext(new JsfExternalContext(event.getFacesContext())); + restoreFlowExecution(event.getFacesContext()); + } + else if (event.getPhaseId() == PhaseId.RENDER_RESPONSE) { if (FlowExecutionHolderUtils.isFlowExecutionRestored(event.getFacesContext())) { prepareResponse(getCurrentContext(), FlowExecutionHolderUtils.getFlowExecutionHolder(event .getFacesContext())); @@ -166,11 +166,7 @@ public class FlowPhaseListener implements PhaseListener { } public void afterPhase(PhaseEvent event) { - if (event.getPhaseId() == PhaseId.RESTORE_VIEW) { - ExternalContextHolder.setExternalContext(new JsfExternalContext(event.getFacesContext())); - restoreFlowExecution(event.getFacesContext()); - } - else if (event.getPhaseId() == PhaseId.RENDER_RESPONSE) { + if (event.getPhaseId() == PhaseId.RENDER_RESPONSE) { try { if (FlowExecutionHolderUtils.isFlowExecutionRestored(event.getFacesContext())) { FlowExecutionHolder holder = FlowExecutionHolderUtils.getFlowExecutionHolder(event @@ -193,29 +189,18 @@ public class FlowPhaseListener implements PhaseListener { protected void restoreFlowExecution(FacesContext facesContext) { JsfExternalContext context = new JsfExternalContext(facesContext); - if (argumentHandler.isFlowExecutionKeyPresent(context) || isFlowExecutionKeyInViewRoot(facesContext)) { - // restore flow execution from repository so it will be - // available to variable/property resolvers and the flow - // navigation handler (this could happen as part of a submission or - // flow execution redirect) + if (argumentHandler.isFlowExecutionKeyPresent(context)) { + // restore flow execution from repository so it will be available to variable/property resolvers + // and the flow navigation handler (this could happen as part of a submission or flow execution redirect) FlowExecutionRepository repository = getRepository(context); - FlowExecutionKey flowExecutionKey; - if (argumentHandler.isFlowExecutionKeyPresent(context)) { - // extract it in the "traditional way" (request parameter in url by default) - flowExecutionKey = repository.parseFlowExecutionKey(argumentHandler.extractFlowExecutionKey(context)); - } - else { - // restore the key from an attribute in the root of the component tree - flowExecutionKey = repository.parseFlowExecutionKey((String) facesContext.getViewRoot().getAttributes() - .get(FLOW_EXECUTION_KEY_VIEW_ROOT_ATTRIBUTE)); - // remove it (it should always be placed back before response rendering) - facesContext.getViewRoot().getAttributes().remove(FLOW_EXECUTION_KEY_VIEW_ROOT_ATTRIBUTE); - } + // extract key in the "traditional way" (request parameter in url by default) + FlowExecutionKey flowExecutionKey = repository.parseFlowExecutionKey(argumentHandler + .extractFlowExecutionKey(context)); FlowExecutionLock lock = repository.getLock(flowExecutionKey); lock.lock(); FlowExecution flowExecution = repository.getFlowExecution(flowExecutionKey); if (logger.isDebugEnabled()) { - logger.debug("Loaded existing flow execution from repository with id '" + flowExecutionKey + "'"); + logger.debug("Loaded existing flow execution from repository with key '" + flowExecutionKey + "'"); } FlowExecutionHolderUtils.setFlowExecutionHolder(new FlowExecutionHolder(flowExecutionKey, flowExecution, lock), facesContext); @@ -311,11 +296,7 @@ public class FlowPhaseListener implements PhaseListener { } String flowExecutionKey = holder.getFlowExecution().isActive() ? holder.getFlowExecutionKey().toString() : null; if (flowExecutionKey != null) { - // expose to view root for preservation in the component tree - if (viewRootAttributeMapPresent(facesContext)) { - facesContext.getViewRoot().getAttributes() - .put(FLOW_EXECUTION_KEY_VIEW_ROOT_ATTRIBUTE, flowExecutionKey); - } + saveInViewRoot(facesContext, flowExecutionKey); } Map requestMap = facesContext.getExternalContext().getRequestMap(); argumentHandler.exposeFlowExecutionContext(flowExecutionKey, holder.getFlowExecution(), requestMap); @@ -353,35 +334,6 @@ public class FlowPhaseListener implements PhaseListener { return (JsfExternalContext) ExternalContextHolder.getExternalContext(); } - /** - * Returns true if the root of the component tree contains the flow execution key attribute, used to restore the - * flow execution on subsequent reqests. - * @param facesContext the key - * @return true if yes, false otherwise - */ - private boolean isFlowExecutionKeyInViewRoot(FacesContext facesContext) { - if (viewRootAttributeMapPresent(facesContext)) { - return facesContext.getViewRoot().getAttributes().containsKey(FLOW_EXECUTION_KEY_VIEW_ROOT_ATTRIBUTE); - } - else { - return false; - } - } - - /** - * Simple little helper that returns true if the view root attribute map is non-null. - * @param facesContext the faces context - * @return true if so, false otherwise - */ - private boolean viewRootAttributeMapPresent(FacesContext facesContext) { - if (facesContext.getViewRoot() != null && facesContext.getViewRoot().getAttributes() != null) { - return true; - } - else { - return false; - } - } - private void updateViewRoot(FacesContext facesContext, String viewId) { UIViewRoot viewRoot = facesContext.getViewRoot(); if (viewRoot == null || hasViewChanged(viewRoot, viewId)) { @@ -396,11 +348,27 @@ public class FlowPhaseListener implements PhaseListener { return !viewRoot.getViewId().equals(viewId); } + /** + * Saves the flow execution key in a component in the view root for restoration on subsequent RESTORE_VIEW operations. + * @param facesContext the faces context exposing the view root + * @param flowExecutionKey the flow execution key + */ + private void saveInViewRoot(FacesContext facesContext, String flowExecutionKey) { + // search for key holder in the component tree + FlowExecutionKeyStateHolder keyHolder = (FlowExecutionKeyStateHolder) facesContext.getViewRoot() + .findComponent(FlowExecutionKeyStateHolder.COMPONENT_ID); + if (keyHolder == null) { + keyHolder = new FlowExecutionKeyStateHolder(); + // expose as the first component in the view root for preservation in the tree + facesContext.getViewRoot().getChildren().add(0, keyHolder); + } + keyHolder.setFlowExecutionKey(flowExecutionKey); + } + private void generateKey(JsfExternalContext context, FlowExecutionHolder holder) { FlowExecution flowExecution = holder.getFlowExecution(); if (flowExecution.isActive()) { - // generate new continuation key for the flow execution - // before rendering the response + // generate new continuation key for the flow execution before rendering the response FlowExecutionKey flowExecutionKey = holder.getFlowExecutionKey(); FlowExecutionRepository repository = getRepository(context); if (flowExecutionKey == null) { @@ -408,8 +376,7 @@ public class FlowPhaseListener implements PhaseListener { flowExecutionKey = repository.generateKey(flowExecution); } else { - // it is an existing conversaiton, use same conversation id, - // generate a new continuation id + // it is an existing conversaiton, use same conversation id, generate a new continuation id flowExecutionKey = repository.getNextKey(flowExecution, flowExecutionKey); } holder.setFlowExecutionKey(flowExecutionKey); @@ -448,15 +415,6 @@ public class FlowPhaseListener implements PhaseListener { } } - /** - * Standard default view id resolver which uses the web flow view name as the jsf view id - */ - public static class DefaultViewIdMapper implements ViewIdMapper { - public String mapViewId(String viewName) { - return viewName; - } - } - private FlowDefinitionLocator getLocator(JsfExternalContext context) { return FlowFacesUtils.getDefinitionLocator(context.getFacesContext()); } @@ -468,4 +426,13 @@ public class FlowPhaseListener implements PhaseListener { private FlowExecutionRepository getRepository(JsfExternalContext context) { return FlowFacesUtils.getExecutionRepository(context.getFacesContext()); } + + /** + * Standard default view id resolver which uses the web flow view name as the jsf view id + */ + public static class DefaultViewIdMapper implements ViewIdMapper { + public String mapViewId(String viewName) { + return viewName; + } + } } \ No newline at end of file