diff --git a/spring-faces/.classpath b/spring-faces/.classpath index 271552c4..b59bd22d 100644 --- a/spring-faces/.classpath +++ b/spring-faces/.classpath @@ -8,8 +8,8 @@ - - + + diff --git a/spring-faces/ivy.xml b/spring-faces/ivy.xml index 28f3082f..89b9d96f 100644 --- a/spring-faces/ivy.xml +++ b/spring-faces/ivy.xml @@ -42,8 +42,8 @@ - - + + diff --git a/spring-faces/pom.xml b/spring-faces/pom.xml index 3e3b75c9..caa8d05a 100644 --- a/spring-faces/pom.xml +++ b/spring-faces/pom.xml @@ -57,12 +57,6 @@ spring-webflow ${project.version} - - com.sun.facelets - jsf-facelets - 1.1.14 - true - org.richfaces.framework richfaces-api @@ -82,15 +76,15 @@ provided - javax.faces + com.sun.faces jsf-api - 2.0 + 2.0.3 provided - javax.faces + com.sun.faces jsf-impl - 2.0.2 + 2.0.3 provided diff --git a/spring-faces/src/main/java/org/springframework/faces/webflow/FlowFacesContext.java b/spring-faces/src/main/java/org/springframework/faces/webflow/FlowFacesContext.java index 96df2c61..1c8bbd74 100644 --- a/spring-faces/src/main/java/org/springframework/faces/webflow/FlowFacesContext.java +++ b/spring-faces/src/main/java/org/springframework/faces/webflow/FlowFacesContext.java @@ -58,6 +58,8 @@ public class FlowFacesContext extends FacesContext { private FlowFacesContextMessageDelegate messageDelegate; + private ExternalContext externalContext; + /** * The base FacesContext delegate */ @@ -77,6 +79,7 @@ public class FlowFacesContext extends FacesContext { this.context = context; this.delegate = delegate; this.messageDelegate = new FlowFacesContextMessageDelegate(context); + this.externalContext = new FlowExternalContext(delegate.getExternalContext()); setCurrentInstance(this); } @@ -160,7 +163,7 @@ public class FlowFacesContext extends FacesContext { } public ExternalContext getExternalContext() { - return new FlowExternalContext(delegate.getExternalContext()); + return externalContext; } public RenderKit getRenderKit() { diff --git a/spring-faces/src/main/java/org/springframework/faces/webflow/FlowFacesContextLifecycleListener.java b/spring-faces/src/main/java/org/springframework/faces/webflow/FlowFacesContextLifecycleListener.java new file mode 100644 index 00000000..8932832f --- /dev/null +++ b/spring-faces/src/main/java/org/springframework/faces/webflow/FlowFacesContextLifecycleListener.java @@ -0,0 +1,48 @@ +/* + * Copyright 2004-2010 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. + */ +package org.springframework.faces.webflow; + +import javax.faces.context.FacesContext; + +import org.springframework.webflow.execution.FlowExecutionListener; +import org.springframework.webflow.execution.FlowExecutionListenerAdapter; +import org.springframework.webflow.execution.RequestContext; + +/** + * A {@link FlowExecutionListener} that creates a {@link FlowFacesContext} instance when a flow request is submitted and + * releases it when the request has been processed. + * + * @author Rossen Stoyanchev + */ +public class FlowFacesContextLifecycleListener extends FlowExecutionListenerAdapter { + + /** + * Creates a new instance of {@link FlowFacesContext} that is then available for the duration of the request. + * @param context the current flow request context + */ + public void requestSubmitted(RequestContext context) { + FlowFacesContext.newInstance(context, FlowLifecycle.newInstance()); + } + + /** + * Releases the current {@link FlowFacesContext} instance. + * @param context the source of the event + */ + public void requestProcessed(RequestContext context) { + FacesContext.getCurrentInstance().release(); + } + +} diff --git a/spring-faces/src/main/java/org/springframework/faces/webflow/FlowRenderKit.java b/spring-faces/src/main/java/org/springframework/faces/webflow/FlowRenderKit.java new file mode 100644 index 00000000..bcf3420b --- /dev/null +++ b/spring-faces/src/main/java/org/springframework/faces/webflow/FlowRenderKit.java @@ -0,0 +1,55 @@ +/* + * Copyright 2004-2010 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. + */ +package org.springframework.faces.webflow; + +/** + * A render kit implementation that ensures use of Web Flow's FlowViewResponseStateManager, which takes over reading and + * writing JSF state and manages that in Web Flow's view scope. The FlowViewResponseStateManager is plugged in only in a + * JSF 2 environment. + * + * @author Rossen Stoyanchev + * @since 2.2.0 + */ +import javax.faces.render.RenderKit; +import javax.faces.render.RenderKitWrapper; +import javax.faces.render.ResponseStateManager; + +public class FlowRenderKit extends RenderKitWrapper { + + private RenderKit delegate; + + private FlowViewResponseStateManager responseStateManager; + + public FlowRenderKit(RenderKit delegate) { + this.delegate = delegate; + if (JsfRuntimeInformation.isAtLeastJsf20()) { + this.responseStateManager = new FlowViewResponseStateManager(delegate.getResponseStateManager()); + } + } + + public RenderKit getWrapped() { + return delegate; + } + + /** + * Returns an instance of {@link FlowViewResponseStateManager} in a JSF 2 environment or returns the delegates's + * ResponseStateManager instance otherwise. + */ + public ResponseStateManager getResponseStateManager() { + return (JsfRuntimeInformation.isAtLeastJsf20()) ? responseStateManager : delegate.getResponseStateManager(); + } + +} diff --git a/spring-faces/src/main/java/org/springframework/faces/webflow/FlowViewResponseStateManager.java b/spring-faces/src/main/java/org/springframework/faces/webflow/FlowViewResponseStateManager.java new file mode 100644 index 00000000..25deb785 --- /dev/null +++ b/spring-faces/src/main/java/org/springframework/faces/webflow/FlowViewResponseStateManager.java @@ -0,0 +1,181 @@ +/* + * Copyright 2004-2010 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. + */ +package org.springframework.faces.webflow; + +import java.io.IOException; +import java.io.Writer; + +import javax.faces.context.FacesContext; +import javax.faces.context.ResponseWriter; +import javax.faces.render.RenderKitFactory; +import javax.faces.render.ResponseStateManager; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.RequestContextHolder; + +/** + *

+ * A custom ResponseStateManager that writes JSF state to a Web Flow managed view-scoped variable. This class is plugged + * in via {@link FlowRenderKit} in JSF 2 runtime environments only. + *

+ * + *

+ * In JSF 2 where a partial state saving algorithm is used, Web Flow delegates to the JSF 2 runtime to handle state + * saving. However, an instance of this class plugged in via {@link FlowRenderKit} will ensure that state is saved in a + * Web Flow managed view-scoped variable. + *

+ * + * @author Rossen Stoyanchev + * @since 2.2.0 + */ +public class FlowViewResponseStateManager extends ResponseStateManager { + + private static final Log logger = LogFactory.getLog(FlowViewResponseStateManager.class); + + private ResponseStateManager delegate; + + private char[] stateFieldStart = ("".toCharArray(); + + public FlowViewResponseStateManager(ResponseStateManager delegate) { + this.delegate = delegate; + } + + /** + *

+ * Wraps state in an instance of {@link FlowSerializedView} and stores it in view scope. + *

+ * + *

+ * Also complies with the contract for {@link ResponseStateManager#writeState(FacesContext, Object)} by writing the + * "javax.faces.ViewState" and optionally the "javax.faces.RenderKitId" hidden input fields to the response. + *

+ */ + @Override + public void writeState(FacesContext facesContext, Object state) throws IOException { + if (!JsfUtils.isFlowRequest()) { + delegate.writeState(facesContext, state); + } + FlowSerializedView view = null; + if (state instanceof FlowSerializedView) { + view = (FlowSerializedView) state; + } else { + Object[] serializedState = (Object[]) state; + view = new FlowSerializedView(facesContext.getViewRoot().getViewId(), serializedState[0], serializedState[1]); + } + RequestContext requestContext = RequestContextHolder.getRequestContext(); + requestContext.getViewScope().put(FlowViewStateManager.SERIALIZED_VIEW_STATE, view); + + ResponseWriter writer = facesContext.getResponseWriter(); + writeViewStateField(facesContext, writer); + writeRenderKitIdField(facesContext, writer); + } + + /** + *

+ * Retrieves the state from view scope as an instance of {@link FlowSerializedView} and turns it to an array before + * returning. + *

+ */ + @Override + public Object getState(FacesContext facesContext, String viewId) { + if (!JsfUtils.isFlowRequest()) { + return delegate.getState(facesContext, viewId); + } + RequestContext requestContext = RequestContextHolder.getRequestContext(); + FlowSerializedView view = (FlowSerializedView) requestContext.getViewScope().get( + FlowViewStateManager.SERIALIZED_VIEW_STATE); + Object[] state = null; + if (view == null) { + logger.debug("No matching view in view scope"); + } else { + state = new Object[] { view.getTreeStructure(), view.getComponentState() }; + } + return state; + } + + /** + * This method returns the flow execution key to be used as the value for the "javax.faces.ViewState" hidden input + * field. The value of this key is not important because JSF state is stored in a Web Flow managed view scoped + * variable. However the presence of the view state parameter alone is important for triggering actions. Hence we + * return the most logical value, which is the flow execution key. + */ + @Override + public String getViewState(FacesContext facesContext, Object state) { + if (!JsfUtils.isFlowRequest()) { + return delegate.getViewState(facesContext, state); + } + return getFlowExecutionKey(); + } + + // ------------------- Delegation methods ------------------// + + @SuppressWarnings( { "deprecation" }) + @Override + public Object getTreeStructureToRestore(FacesContext context, String viewId) { + return delegate.getTreeStructureToRestore(context, viewId); + } + + @SuppressWarnings( { "deprecation" }) + @Override + public Object getComponentStateToRestore(FacesContext context) { + return delegate.getComponentStateToRestore(context); + } + + // ------------------- Private helper methods ------------------// + + private String getFlowExecutionKey() { + RequestContext requestContext = RequestContextHolder.getRequestContext(); + return requestContext.getFlowExecutionContext().getKey().toString(); + } + + /** + * See comments on {@link ResponseStateManager#VIEW_STATE_PARAM}. + * + * @param context the FacesContext for the current request + * @param writer the ResponseWriter to write to + * @throws IOException if an error occurs writing to the client + */ + private void writeViewStateField(FacesContext context, Writer writer) throws IOException { + writer.write(stateFieldStart); + writer.write(getFlowExecutionKey()); + writer.write(stateFieldEnd); + } + + /** + * See comments on ResponseStateManager.RENDER_KIT_ID_PARAM in + * {@link ResponseStateManager#writeState(FacesContext, Object)}. + * + * @param context the FacesContext for the current request + * @param writer the ResponseWriter to write to + * @throws IOException if an error occurs writing to the client + */ + private void writeRenderKitIdField(FacesContext context, ResponseWriter writer) throws IOException { + String result = context.getApplication().getDefaultRenderKitId(); + if (result != null && !RenderKitFactory.HTML_BASIC_RENDER_KIT.equals(result)) { + writer.startElement("input", context.getViewRoot()); + writer.writeAttribute("type", "hidden", "type"); + writer.writeAttribute("name", ResponseStateManager.RENDER_KIT_ID_PARAM, "name"); + writer.writeAttribute("value", result, "value"); + writer.endElement("input"); + } + } + +} diff --git a/spring-faces/src/main/java/org/springframework/faces/webflow/FlowViewStateManager.java b/spring-faces/src/main/java/org/springframework/faces/webflow/FlowViewStateManager.java index 9c81325b..fcbacb15 100644 --- a/spring-faces/src/main/java/org/springframework/faces/webflow/FlowViewStateManager.java +++ b/spring-faces/src/main/java/org/springframework/faces/webflow/FlowViewStateManager.java @@ -15,6 +15,8 @@ */ package org.springframework.faces.webflow; +import static org.springframework.faces.webflow.JsfRuntimeInformation.isAtLeastJsf20; + import java.io.IOException; import javax.faces.application.StateManager; @@ -35,7 +37,7 @@ public class FlowViewStateManager extends StateManager { private static final Log logger = LogFactory.getLog(FlowViewStateManager.class); - private static final String SERIALIZED_VIEW_STATE = "flowSerializedViewState"; + protected static final String SERIALIZED_VIEW_STATE = "flowSerializedViewState"; private StateManager delegate; @@ -136,49 +138,78 @@ public class FlowViewStateManager extends StateManager { if (!JsfUtils.isFlowRequest()) { return delegate.saveSerializedView(context); } - FlowSerializedView view = (FlowSerializedView) saveView(context); - return new javax.faces.application.StateManager.SerializedView(view.getTreeStructure(), view - .getComponentState()); + Object state = saveView(context); + if (state instanceof FlowSerializedView) { + FlowSerializedView serializedState = (FlowSerializedView) state; + return new javax.faces.application.StateManager.SerializedView(serializedState.getTreeStructure(), + serializedState.getComponentState()); + } else { + Object[] serializedState = (Object[]) state; + return new javax.faces.application.StateManager.SerializedView(serializedState[0], serializedState[1]); + } } /** - * JSF 1.2 version of state saving + *

+ * JSF 1.2 (or higher) version of state saving. + *

+ * + *

+ * In JSF 2 where a partial state saving algorithm is used, this method merely delegates to the next + * ViewStateManager. Thus partial state saving is handled by the JSF 2 runtime. However, a + * {@link FlowViewResponseStateManager} plugged in via {@link FlowRenderKit} will ensure the state is saved in a Web + * Flow view-scoped variable. + *

*/ public Object saveView(FacesContext context) { if (context.getViewRoot().isTransient()) { return null; } - if (!JsfUtils.isFlowRequest()) { + if ((!JsfUtils.isFlowRequest()) || isAtLeastJsf20()) { return delegate.saveView(context); + } else { + RequestContext requestContext = RequestContextHolder.getRequestContext(); + if (logger.isDebugEnabled()) { + logger.debug("Saving view root '" + context.getViewRoot().getViewId() + "' in view scope"); + } + FlowSerializedView view = new FlowSerializedView(context.getViewRoot().getViewId(), + getTreeStructureToSave(context), getComponentStateToSave(context)); + requestContext.getViewScope().put(SERIALIZED_VIEW_STATE, view); + return view; } - RequestContext requestContext = RequestContextHolder.getRequestContext(); - if (logger.isDebugEnabled()) { - logger.debug("Saving view root '" + context.getViewRoot().getViewId() + "' in view scope"); - } - FlowSerializedView view = new FlowSerializedView(context.getViewRoot().getViewId(), - getTreeStructureToSave(context), getComponentStateToSave(context)); - requestContext.getViewScope().put(SERIALIZED_VIEW_STATE, view); - return view; } + /** + *

+ * In JSF 2 where a partial state saving algorithm is used, this method merely delegates to the next + * ViewStateManager. Thus partial state saving is handled by the JSF 2 runtime. However, a + * {@link FlowViewResponseStateManager} plugged in via {@link FlowRenderKit} will ensure the state is saved in a Web + * Flow view-scoped variable. + *

+ */ public UIViewRoot restoreView(FacesContext context, String viewId, String renderKitId) { - if (!JsfUtils.isFlowRequest()) { + if ((!JsfUtils.isFlowRequest()) || isAtLeastJsf20()) { return delegate.restoreView(context, viewId, renderKitId); + } else { + UIViewRoot viewRoot = restoreTreeStructure(context, viewId, renderKitId); + if (viewRoot != null) { + context.setViewRoot(viewRoot); + restoreComponentState(context, viewRoot, renderKitId); + } + return viewRoot; } - UIViewRoot viewRoot = restoreTreeStructure(context, viewId, renderKitId); - if (viewRoot != null) { - context.setViewRoot(viewRoot); - restoreComponentState(context, viewRoot, renderKitId); - } - return viewRoot; } @Override public String getViewState(FacesContext context) { + if (!JsfUtils.isFlowRequest()) { + return delegate.getViewState(context); + } /* - * Mojarra 2: PartialRequestContextImpl.renderState() invokes this method during Ajax request rendering. It is - * overridden to convert FlowSerializedView to an array containing tree structure and component state. The - * ResponseStateManager.getViewState() calls the ServerSideStateHelper, which expects the array. + * Mojarra 2: PartialRequestContextImpl.renderState() invokes this method during Ajax request rendering. We + * overridde it to convert FlowSerializedView state to an array before calling the + * ResponseStateManager.getViewState(), which in turn calls the ServerSideStateHelper and expects state to be an + * array. */ Object state = saveView(context); if (state != null) { diff --git a/spring-faces/src/main/java/org/springframework/faces/webflow/Jsf2FlowFacesContext.java b/spring-faces/src/main/java/org/springframework/faces/webflow/Jsf2FlowFacesContext.java index 742403de..fc4e5bcc 100644 --- a/spring-faces/src/main/java/org/springframework/faces/webflow/Jsf2FlowFacesContext.java +++ b/spring-faces/src/main/java/org/springframework/faces/webflow/Jsf2FlowFacesContext.java @@ -32,6 +32,8 @@ import javax.faces.context.PartialViewContext; import javax.faces.context.PartialViewContextFactory; import javax.faces.event.PhaseId; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.webflow.execution.RequestContext; /** @@ -41,6 +43,8 @@ import org.springframework.webflow.execution.RequestContext; */ public class Jsf2FlowFacesContext extends FlowFacesContext { + private ExternalContext externalContext; + /* * This partialViewContext duplicates the one FacesContextImpl because the constructor of FacesContextImpl calls * getPartialViewContext(), which causes it to be instantiated with an instance of FacesContextImpl. This leads to @@ -52,13 +56,15 @@ public class Jsf2FlowFacesContext extends FlowFacesContext { public Jsf2FlowFacesContext(RequestContext context, FacesContext delegate) { super(context, delegate); + this.externalContext = new Jsf2FlowExternalContext(getDelegate().getExternalContext()); + PartialViewContextFactory f = (PartialViewContextFactory) FactoryFinder .getFactory(FactoryFinder.PARTIAL_VIEW_CONTEXT_FACTORY); - partialViewContext = f.getPartialViewContext(this); + this.partialViewContext = f.getPartialViewContext(this); } public ExternalContext getExternalContext() { - return new Jsf2FlowExternalContext(getDelegate().getExternalContext()); + return externalContext; } // --------------- JSF 2.0 Pass-through delegate methods ------------------// @@ -128,10 +134,17 @@ public class Jsf2FlowFacesContext extends FlowFacesContext { protected class Jsf2FlowExternalContext extends FlowExternalContext { + Log logger = LogFactory.getLog(FlowExternalContext.class); + public Jsf2FlowExternalContext(ExternalContext delegate) { super(delegate); } + public void responseSendError(int statusCode, String message) throws IOException { + logger.debug("Sending error HTTP status code " + statusCode + " with message '" + message + "'"); + delegate.responseSendError(statusCode, message); + } + // --------------- JSF 2.0 Pass-through delegate methods ------------------// public String getContextName() { @@ -210,10 +223,6 @@ public class Jsf2FlowFacesContext extends FlowFacesContext { delegate.responseReset(); } - public void responseSendError(int statusCode, String message) throws IOException { - delegate.responseSendError(statusCode, message); - } - public void setResponseStatus(int statusCode) { delegate.setResponseStatus(statusCode); } diff --git a/spring-faces/src/main/java/org/springframework/faces/webflow/JsfView.java b/spring-faces/src/main/java/org/springframework/faces/webflow/JsfView.java index d9643d43..ed7cfb63 100644 --- a/spring-faces/src/main/java/org/springframework/faces/webflow/JsfView.java +++ b/spring-faces/src/main/java/org/springframework/faces/webflow/JsfView.java @@ -16,6 +16,7 @@ package org.springframework.faces.webflow; import static org.springframework.faces.webflow.JsfRuntimeInformation.isAtLeastJsf12; +import static org.springframework.faces.webflow.JsfRuntimeInformation.isLessThanJsf20; import java.io.IOException; import java.io.Serializable; @@ -79,7 +80,7 @@ public class JsfView implements View { * Performs the standard duties of the JSF RENDER_RESPONSE phase. */ public void render() throws IOException { - FacesContext facesContext = FlowFacesContext.newInstance(requestContext, facesLifecycle); + FacesContext facesContext = FlowFacesContext.getCurrentInstance(); if (facesContext.getResponseComplete()) { return; } @@ -90,7 +91,6 @@ public class JsfView implements View { } finally { logger.debug("View rendering complete"); facesContext.responseComplete(); - facesContext.release(); } } @@ -107,15 +107,11 @@ public class JsfView implements View { * INVOKE_APPLICATION. */ public void processUserEvent() { - FacesContext facesContext = FlowFacesContext.newInstance(requestContext, facesLifecycle); + FacesContext facesContext = FlowFacesContext.getCurrentInstance(); facesContext.setViewRoot(viewRoot); - try { - // Must respect these flags in case user set them during RESTORE_VIEW phase - if (!facesContext.getRenderResponse() && !facesContext.getResponseComplete()) { - facesLifecycle.execute(facesContext); - } - } finally { - facesContext.release(); + // Must respect these flags in case user set them during RESTORE_VIEW phase + if (!facesContext.getRenderResponse() && !facesContext.getResponseComplete()) { + facesLifecycle.execute(facesContext); } } @@ -124,22 +120,26 @@ public class JsfView implements View { * snapshot */ public void saveState() { - FacesContext facesContext = FlowFacesContext.newInstance(requestContext, facesLifecycle); + FacesContext facesContext = FlowFacesContext.getCurrentInstance(); if (viewRoot instanceof AjaxViewRoot) { facesContext.setViewRoot(((AjaxViewRoot) viewRoot).getOriginalViewRoot()); } else { facesContext.setViewRoot(viewRoot); } - try { - facesContext.getApplication().getStateManager().saveSerializedView(facesContext); - } finally { - facesContext.release(); - } + facesContext.getApplication().getStateManager().saveSerializedView(facesContext); } public Serializable getUserEventState() { - // Set the temporary UIViewRoot state so that it will be available across the redirect - return new ViewRootHolder(getViewRoot()); + if (isLessThanJsf20()) { + // Set the temporary UIViewRoot state so that it will be available across the redirect + return new ViewRootHolder(getViewRoot()); + } else { + // In JSF 2 the partial state saving algorithm attaches a system event listener to the UIViewRoot with + // a reference to the FacesContext instance. The FacesContext instance is released at end of each request. + // Hence, keeping the UIViewRoot across the redirect is not feasible. + logger.debug("User event state requested but not saved."); + return null; + } } public boolean hasFlowEvent() { 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 ae4e6594..1d9a4f45 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 @@ -17,7 +17,6 @@ package org.springframework.faces.webflow; import static org.springframework.faces.webflow.JsfRuntimeInformation.isAtLeastJsf12; import static org.springframework.faces.webflow.JsfRuntimeInformation.isAtLeastJsf20; -import static org.springframework.faces.webflow.JsfRuntimeInformation.isLessThanJsf20; import static org.springframework.faces.webflow.JsfRuntimeInformation.isPortletRequest; import java.util.Iterator; @@ -72,65 +71,61 @@ public class JsfViewFactory implements ViewFactory { * be rendered in the case of an executing transition. */ public View getView(RequestContext context) { - FacesContext facesContext = FlowFacesContext.newInstance(context, lifecycle); - try { - if (isAtLeastJsf20()) { - facesContext.setCurrentPhaseId(PhaseId.RESTORE_VIEW); + FacesContext facesContext = FlowFacesContext.getCurrentInstance(); + if (isAtLeastJsf20()) { + facesContext.setCurrentPhaseId(PhaseId.RESTORE_VIEW); + } + if (!facesContext.getRenderResponse()) { + // only publish a RESTORE_VIEW event if this is the first phase of the lifecycle + // this won't be true when this method is called after a transition from one view-state to another + JsfUtils.notifyBeforeListeners(PhaseId.RESTORE_VIEW, lifecycle, facesContext); + } + ViewHandler viewHandler = facesContext.getApplication().getViewHandler(); + if (isAtLeastJsf12() && (!isPortletRequest(facesContext))) { + viewHandler.initView(facesContext); + } + JsfView view; + String viewName = (String) viewIdExpression.getValue(context); + if (viewAlreadySet(facesContext, viewName)) { + if (logger.isDebugEnabled()) { + logger.debug("Existing view root found with id '" + facesContext.getViewRoot().getId() + "'"); } - if (!facesContext.getRenderResponse()) { - // only publish a RESTORE_VIEW event if this is the first phase of the lifecycle - // this won't be true when this method is called after a transition from one view-state to another - JsfUtils.notifyBeforeListeners(PhaseId.RESTORE_VIEW, lifecycle, facesContext); - } - ViewHandler viewHandler = facesContext.getApplication().getViewHandler(); - if (isAtLeastJsf12() && (!isPortletRequest(facesContext))) { - viewHandler.initView(facesContext); - } - JsfView view; - String viewName = (String) viewIdExpression.getValue(context); - if (viewAlreadySet(facesContext, viewName)) { - if (logger.isDebugEnabled()) { - logger.debug("Existing view root found with id '" + facesContext.getViewRoot().getId() + "'"); - } - UIViewRoot viewRoot = facesContext.getViewRoot(); - viewRoot.setLocale(context.getExternalContext().getLocale()); - processTree(facesContext, viewRoot); - view = createJsfView(facesContext.getViewRoot(), lifecycle, context); - } else { - if (context.inViewState()) { - UIViewRoot viewRoot = viewHandler.restoreView(facesContext, viewName); - if (viewRoot != null) { - if (logger.isDebugEnabled()) { - logger.debug("UIViewRoot restored for '" + viewName + "'"); - } - facesContext.setViewRoot(viewRoot); - processTree(facesContext, viewRoot); - view = createJsfView(viewRoot, lifecycle, context); - } else { - if (logger.isDebugEnabled()) { - logger.debug("Creating UIViewRoot from '" + viewName + "'"); - } - viewRoot = viewHandler.createView(facesContext, viewName); - facesContext.setViewRoot(viewRoot); - view = createJsfView(viewRoot, lifecycle, context); + UIViewRoot viewRoot = facesContext.getViewRoot(); + viewRoot.setLocale(context.getExternalContext().getLocale()); + processTree(facesContext, viewRoot); + view = createJsfView(facesContext.getViewRoot(), lifecycle, context); + } else { + if (context.inViewState()) { + UIViewRoot viewRoot = viewHandler.restoreView(facesContext, viewName); + if (viewRoot != null) { + if (logger.isDebugEnabled()) { + logger.debug("UIViewRoot restored for '" + viewName + "'"); } + facesContext.setViewRoot(viewRoot); + processTree(facesContext, viewRoot); + view = createJsfView(viewRoot, lifecycle, context); } else { if (logger.isDebugEnabled()) { - logger.debug("Creating transient UIViewRoot from '" + viewName + "'"); + logger.debug("Creating UIViewRoot from '" + viewName + "'"); } - UIViewRoot viewRoot = viewHandler.createView(facesContext, viewName); - viewRoot.setTransient(true); + viewRoot = viewHandler.createView(facesContext, viewName); facesContext.setViewRoot(viewRoot); view = createJsfView(viewRoot, lifecycle, context); } + } else { + if (logger.isDebugEnabled()) { + logger.debug("Creating transient UIViewRoot from '" + viewName + "'"); + } + UIViewRoot viewRoot = viewHandler.createView(facesContext, viewName); + viewRoot.setTransient(true); + facesContext.setViewRoot(viewRoot); + view = createJsfView(viewRoot, lifecycle, context); } - if (!facesContext.getRenderResponse()) { - JsfUtils.notifyAfterListeners(PhaseId.RESTORE_VIEW, lifecycle, facesContext); - } - return view; - } finally { - facesContext.release(); } + if (!facesContext.getRenderResponse()) { + JsfUtils.notifyAfterListeners(PhaseId.RESTORE_VIEW, lifecycle, facesContext); + } + return view; } private boolean viewAlreadySet(FacesContext facesContext, String viewName) { @@ -143,7 +138,7 @@ public class JsfViewFactory implements ViewFactory { } private JsfView createJsfView(UIViewRoot root, Lifecycle lifecycle, RequestContext context) { - if (isLessThanJsf20() && isSpringJavascriptAjaxRequest(context.getExternalContext())) { + if (isSpringJavascriptAjaxRequest(context.getExternalContext())) { root = new AjaxViewRoot(root); } return new JsfView(root, lifecycle, context); diff --git a/spring-faces/src/main/resources/META-INF/faces-config.xml b/spring-faces/src/main/resources/META-INF/faces-config.xml index 0ef11f11..c5a1cb66 100644 --- a/spring-faces/src/main/resources/META-INF/faces-config.xml +++ b/spring-faces/src/main/resources/META-INF/faces-config.xml @@ -88,6 +88,7 @@ HTML_BASIC + org.springframework.faces.webflow.FlowRenderKit javax.faces.Command diff --git a/spring-faces/src/test/java/org/springframework/faces/ui/ProgressiveCommandLinkRendererTests.java b/spring-faces/src/test/java/org/springframework/faces/ui/ProgressiveCommandLinkRendererTests.java index 93e1ec4c..cedb8036 100644 --- a/spring-faces/src/test/java/org/springframework/faces/ui/ProgressiveCommandLinkRendererTests.java +++ b/spring-faces/src/test/java/org/springframework/faces/ui/ProgressiveCommandLinkRendererTests.java @@ -1,13 +1,10 @@ package org.springframework.faces.ui; -import java.io.StringWriter; - import javax.faces.component.UIForm; import javax.faces.component.UIParameter; import junit.framework.TestCase; -import org.apache.myfaces.test.mock.MockResponseWriter; import org.springframework.faces.webflow.JSFMockHelper; public class ProgressiveCommandLinkRendererTests extends TestCase { @@ -16,15 +13,11 @@ public class ProgressiveCommandLinkRendererTests extends TestCase { ProgressiveCommandLinkRenderer renderer = new ProgressiveCommandLinkRenderer(); - StringWriter output = new StringWriter(); - public void setUp() throws Exception { jsf.setUp(); - jsf.facesContext().setResponseWriter(new MockResponseWriter(output, null, null)); } public void tearDown() throws Exception { - System.out.println(output); jsf.tearDown(); } @@ -47,7 +40,7 @@ public class ProgressiveCommandLinkRendererTests extends TestCase { jsf.facesContext().getResponseWriter().endElement("a"); - assertEquals(expected, output.toString()); + assertEquals(expected, jsf.contentAsString()); } public void testRenderOnClick_AjaxEnabled_WithParams() throws Exception { @@ -78,7 +71,7 @@ public class ProgressiveCommandLinkRendererTests extends TestCase { jsf.facesContext().getResponseWriter().endElement("a"); - assertEquals(expected, output.toString()); + assertEquals(expected, jsf.contentAsString()); } public void testRenderOnClick_AjaxDisabled_NoParams() throws Exception { @@ -100,7 +93,7 @@ public class ProgressiveCommandLinkRendererTests extends TestCase { jsf.facesContext().getResponseWriter().endElement("a"); - assertEquals(expected, output.toString()); + assertEquals(expected, jsf.contentAsString()); } public void testRenderOnClick_AjaxDisabled_WithParams() throws Exception { @@ -132,6 +125,6 @@ public class ProgressiveCommandLinkRendererTests extends TestCase { jsf.facesContext().getResponseWriter().endElement("a"); - assertEquals(expected, output.toString()); + assertEquals(expected, jsf.contentAsString()); } } 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 74fdf966..c73b6d0b 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 @@ -1,5 +1,6 @@ package org.springframework.faces.webflow; +import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; @@ -19,8 +20,10 @@ import org.apache.myfaces.test.mock.MockHttpServletRequest; import org.apache.myfaces.test.mock.MockHttpServletResponse; import org.apache.myfaces.test.mock.MockHttpSession; import org.apache.myfaces.test.mock.MockPartialViewContextFactory; +import org.apache.myfaces.test.mock.MockPrintWriter; import org.apache.myfaces.test.mock.MockRenderKit; import org.apache.myfaces.test.mock.MockRenderKitFactory; +import org.apache.myfaces.test.mock.MockResponseWriter; import org.apache.myfaces.test.mock.MockServletConfig; import org.apache.myfaces.test.mock.MockServletContext; import org.apache.myfaces.test.mock.lifecycle.MockLifecycle; @@ -45,6 +48,10 @@ public class JSFMockHelper { return mock.config(); } + public String contentAsString() throws IOException { + return mock.contentAsString(); + } + public MockExternalContext externalContext() { return mock.externalContext(); } @@ -132,6 +139,8 @@ public class JSFMockHelper { facesContextFactory = (FacesContextFactory) FactoryFinder.getFactory(FactoryFinder.FACES_CONTEXT_FACTORY); facesContext = facesContextFactory.getFacesContext(servletContext, request, response, lifecycle); externalContext = (MockExternalContext) facesContext.getExternalContext(); + facesContext.setResponseWriter(new MockResponseWriter(response.getWriter())); + UIViewRoot root = new UIViewRoot(); root.setViewId("/viewId"); root.setRenderKitId(RenderKitFactory.HTML_BASIC_RENDER_KIT); @@ -174,6 +183,11 @@ public class JSFMockHelper { return config; } + public String contentAsString() throws IOException { + MockPrintWriter writer = (MockPrintWriter) response.getWriter(); + return new String(writer.content()); + } + public MockExternalContext externalContext() { return externalContext; } diff --git a/spring-faces/src/test/java/org/springframework/faces/webflow/JsfAjaxHandlerTests.java b/spring-faces/src/test/java/org/springframework/faces/webflow/JsfAjaxHandlerTests.java index 051a3378..544fb019 100644 --- a/spring-faces/src/test/java/org/springframework/faces/webflow/JsfAjaxHandlerTests.java +++ b/spring-faces/src/test/java/org/springframework/faces/webflow/JsfAjaxHandlerTests.java @@ -1,10 +1,7 @@ package org.springframework.faces.webflow; -import java.io.IOException; - import junit.framework.TestCase; -import org.apache.myfaces.test.mock.MockPrintWriter; import org.springframework.web.context.support.StaticWebApplicationContext; public class JsfAjaxHandlerTests extends TestCase { @@ -27,13 +24,8 @@ public class JsfAjaxHandlerTests extends TestCase { public void testSendAjaxRedirect() throws Exception { ajaxHandler.sendAjaxRedirectInternal("/target", jsfMock.request(), jsfMock.response(), false); assertEquals( - "\n", - extractResponseContent()); - } - - private String extractResponseContent() throws IOException { - MockPrintWriter writer = (MockPrintWriter) jsfMock.response().getWriter(); - return new String(writer.content()); + "\n", + jsfMock.contentAsString()); } } diff --git a/spring-faces/src/test/java/org/springframework/faces/webflow/JsfViewTests.java b/spring-faces/src/test/java/org/springframework/faces/webflow/JsfViewTests.java index 1516b078..f4df7b0d 100644 --- a/spring-faces/src/test/java/org/springframework/faces/webflow/JsfViewTests.java +++ b/spring-faces/src/test/java/org/springframework/faces/webflow/JsfViewTests.java @@ -115,8 +115,6 @@ public class JsfViewTests extends TestCase { EasyMock.replay(new Object[] { context, flowExecutionContext, flowMap, flashScope }); view.render(); - - assertNull("The FacesContext was not released", FacesContext.getCurrentInstance()); } public final void testRenderException() throws IOException { @@ -131,7 +129,6 @@ public class JsfViewTests extends TestCase { try { view.render(); } catch (Exception ex) { - assertNull("The FacesContext was not released", FacesContext.getCurrentInstance()); } } diff --git a/spring-faces/src/test/java/org/springframework/faces/webflow/MockBaseFacesContextFactory.java b/spring-faces/src/test/java/org/springframework/faces/webflow/MockBaseFacesContextFactory.java index 888564c0..2da2e12c 100644 --- a/spring-faces/src/test/java/org/springframework/faces/webflow/MockBaseFacesContextFactory.java +++ b/spring-faces/src/test/java/org/springframework/faces/webflow/MockBaseFacesContextFactory.java @@ -10,6 +10,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.myfaces.test.mock.MockExternalContext20; +import org.apache.myfaces.test.mock.MockHttpServletResponse; public class MockBaseFacesContextFactory extends FacesContextFactory { @@ -22,7 +23,25 @@ public class MockBaseFacesContextFactory extends FacesContextFactory { } else { ExternalContext ext = new MockExternalContext20((ServletContext) context, (HttpServletRequest) request, - (HttpServletResponse) response); + (HttpServletResponse) response) { + + public void setResponseBufferSize(int size) { + ((MockHttpServletResponse) getResponse()).setBufferSize(size); + } + + public void setResponseContentLength(int length) { + ((MockHttpServletResponse) getResponse()).setContentLength(length); + } + + public void setResponseContentType(String contentType) { + ((MockHttpServletResponse) getResponse()).setContentType(contentType); + } + + public void setResponseStatus(int statusCode) { + ((MockHttpServletResponse) getResponse()).setStatus(statusCode); + } + + }; return new MockBaseFacesContext(ext, lifecycle); } diff --git a/spring-webflow-samples/booking-faces/.classpath b/spring-webflow-samples/booking-faces/.classpath index f85cdb95..c84dc702 100755 --- a/spring-webflow-samples/booking-faces/.classpath +++ b/spring-webflow-samples/booking-faces/.classpath @@ -8,6 +8,7 @@ + diff --git a/spring-webflow-samples/booking-faces/ivy.xml b/spring-webflow-samples/booking-faces/ivy.xml index b1d136bb..2dc658ee 100755 --- a/spring-webflow-samples/booking-faces/ivy.xml +++ b/spring-webflow-samples/booking-faces/ivy.xml @@ -21,7 +21,6 @@ - @@ -49,21 +48,8 @@ - - - - - + diff --git a/spring-webflow-samples/booking-faces/pom.xml b/spring-webflow-samples/booking-faces/pom.xml index 5d83895e..3abdb720 100644 --- a/spring-webflow-samples/booking-faces/pom.xml +++ b/spring-webflow-samples/booking-faces/pom.xml @@ -153,12 +153,6 @@ 2.0.2 - - javax.el - com.springsource.javax.el - 1.0.0 - provided - javax.servlet com.springsource.javax.servlet diff --git a/spring-webflow-samples/booking-faces/src/main/java/org/springframework/webflow/samples/booking/Booking.java b/spring-webflow-samples/booking-faces/src/main/java/org/springframework/webflow/samples/booking/Booking.java index 98079fb3..8e7875c5 100755 --- a/spring-webflow-samples/booking-faces/src/main/java/org/springframework/webflow/samples/booking/Booking.java +++ b/spring-webflow-samples/booking-faces/src/main/java/org/springframework/webflow/samples/booking/Booking.java @@ -15,9 +15,6 @@ import javax.persistence.ManyToOne; import javax.persistence.Temporal; import javax.persistence.TemporalType; import javax.persistence.Transient; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Pattern; -import javax.validation.constraints.Size; import org.springframework.binding.message.MessageBuilder; import org.springframework.binding.message.MessageContext; @@ -37,17 +34,12 @@ public class Booking implements Serializable { private Hotel hotel; - @NotNull private Date checkinDate; - @NotNull private Date checkoutDate; - @Pattern(regexp = "[0-9]{16}", message = "Credit card number must be 16 digits.") private String creditCard; - @NotNull - @Size(min = 3, message = "A valid credit card name is required.") private String creditCardName; private int creditCardExpiryMonth; diff --git a/spring-webflow-samples/booking-faces/src/main/webapp/WEB-INF/config/webflow-config.xml b/spring-webflow-samples/booking-faces/src/main/webapp/WEB-INF/config/webflow-config.xml index 6addb4ff..876d4388 100644 --- a/spring-webflow-samples/booking-faces/src/main/webapp/WEB-INF/config/webflow-config.xml +++ b/spring-webflow-samples/booking-faces/src/main/webapp/WEB-INF/config/webflow-config.xml @@ -13,7 +13,8 @@ - + + @@ -26,7 +27,10 @@ - + + + + @@ -35,5 +39,5 @@ - + \ No newline at end of file diff --git a/spring-webflow-samples/booking-faces/src/main/webapp/WEB-INF/faces-config-12.xml b/spring-webflow-samples/booking-faces/src/main/webapp/WEB-INF/faces-config-12.xml deleted file mode 100644 index a1d74854..00000000 --- a/spring-webflow-samples/booking-faces/src/main/webapp/WEB-INF/faces-config-12.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - com.sun.facelets.FaceletViewHandler - - \ No newline at end of file diff --git a/spring-webflow-samples/booking-faces/src/main/webapp/WEB-INF/layouts/standard.xhtml b/spring-webflow-samples/booking-faces/src/main/webapp/WEB-INF/layouts/standard.xhtml index 4464eec0..86a65b61 100644 --- a/spring-webflow-samples/booking-faces/src/main/webapp/WEB-INF/layouts/standard.xhtml +++ b/spring-webflow-samples/booking-faces/src/main/webapp/WEB-INF/layouts/standard.xhtml @@ -4,13 +4,15 @@ xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html" xmlns:c="http://java.sun.com/jsp/jstl/core" - xmlns:sf="http://www.springframework.org/tags/faces" contentType="text/html" encoding="UTF-8"> Spring Faces: Hotel Booking Sample Application - + - - javax.faces.PARTIAL_STATE_SAVING - false - - charEncodingFilter @@ -56,11 +50,11 @@ - + springSecurityFilterChain org.springframework.web.filter.DelegatingFilterProxy - + springSecurityFilterChain /* @@ -71,19 +65,6 @@ org.springframework.web.context.ContextLoaderListener - - - Resources Servlet - org.springframework.js.resource.ResourceServlet - 0 - - - - - Resources Servlet - /resources/* - - Spring MVC Dispatcher Servlet diff --git a/spring-webflow-samples/booking-portlet-faces/src/main/webapp/WEB-INF/config/hotelbooking-portlet-config.xml b/spring-webflow-samples/booking-portlet-faces/src/main/webapp/WEB-INF/config/hotelbooking-portlet-config.xml index 7b13834e..ba4a43d6 100644 --- a/spring-webflow-samples/booking-portlet-faces/src/main/webapp/WEB-INF/config/hotelbooking-portlet-config.xml +++ b/spring-webflow-samples/booking-portlet-faces/src/main/webapp/WEB-INF/config/hotelbooking-portlet-config.xml @@ -36,6 +36,7 @@ + @@ -49,6 +50,9 @@ + + + diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/ViewState.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/ViewState.java index c7188d5d..598640ab 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/ViewState.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/ViewState.java @@ -61,6 +61,12 @@ public class ViewState extends TransitionableState { */ private Boolean redirect; + /** + * Whether or not a redirect should occur when the state is not exited (e.g. invalid form submission, a transition + * without a "to" attribute). + */ + private Boolean redirectInSameState = Boolean.FALSE; + /** * Whether or not the view should render as a popup. */ @@ -122,7 +128,7 @@ public class ViewState extends TransitionableState { * Returns whether this view state should request a flow execution redirect when entered. */ public boolean getRedirect() { - return redirect.booleanValue(); + return (redirect != null) ? redirect.booleanValue() : false; } /** @@ -133,6 +139,22 @@ public class ViewState extends TransitionableState { this.redirect = redirect; } + /** + * Returns whether this view state should request a flow execution redirect when the state hasn't been exited. + */ + public boolean getRedirectInSameState() { + return (redirectInSameState != null) ? redirectInSameState.booleanValue() : false; + } + + /** + * Sets whether this view state should requests a flow execution redirect when entered when processing is done but + * the state hasn't been exited (e.g. invalid form submissions). + * @param redirectInSameState the redirect flag + */ + public void setRedirectInSameState(Boolean redirectInSameState) { + this.redirectInSameState = redirectInSameState; + } + /** * Returns whether this view state should render as a popup. */ @@ -206,7 +228,7 @@ public class ViewState extends TransitionableState { if (externalContext.isAjaxRequest()) { render(context, view); } else { - if (shouldRedirect(context)) { + if (shouldRedirectInSameState(context)) { context.getFlashScope().put(View.USER_EVENT_STATE_ATTRIBUTE, view.getUserEventState()); externalContext.requestFlowExecutionRedirect(); } else { @@ -270,6 +292,14 @@ public class ViewState extends TransitionableState { } } + private boolean shouldRedirectInSameState(RequestControlContext context) { + if (redirectInSameState != null) { + return redirectInSameState.booleanValue(); + } else { + return shouldRedirect(context); + } + } + private void render(RequestControlContext context, View view) throws ViewRenderingException { if (logger.isDebugEnabled()) { logger.debug("Rendering + " + view); diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowArtifactFactory.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowArtifactFactory.java index 5b28218e..06e5fa56 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowArtifactFactory.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowArtifactFactory.java @@ -36,8 +36,8 @@ import org.springframework.webflow.execution.Action; import org.springframework.webflow.execution.ViewFactory; /** - * A factory for core web flow elements such as {@link Flow flows}, {@link State states}, and - * {@link Transition transitions}. + * A factory for core web flow elements such as {@link Flow flows}, {@link State states}, and {@link Transition + * transitions}. *

* This factory encapsulates the construction of each Flow implementation as well as each core artifact type. Subclasses * may customize how the core elements are created. @@ -86,6 +86,7 @@ public class FlowArtifactFactory { ViewState viewState = new ViewState(flow, id, viewFactory); viewState.addVariables(variables); viewState.setRedirect(redirect); + viewState.setRedirectInSameState(Boolean.FALSE); viewState.setPopup(popup); viewState.getRenderActionList().addAll(renderActions); configureCommonProperties(viewState, entryActions, transitions, exceptionHandlers, exitActions, attributes);