From 73eaeeded366c79e81fc3cdb860e394e45409a20 Mon Sep 17 00:00:00 2001 From: Erwin Vervaet Date: Fri, 6 Apr 2007 09:33:39 +0000 Subject: [PATCH] Added FlowIdMappingArgumentHandlerWrapper (SWF-204). --- spring-webflow/changelog.txt | 1 + .../FlowIdMappingArgumentHandlerWrapper.java | 186 ++++++++++++++++++ ...wIdMappingArgumentHandlerWrapperTests.java | 154 +++++++++++++++ 3 files changed, 341 insertions(+) create mode 100644 spring-webflow/src/main/java/org/springframework/webflow/executor/support/FlowIdMappingArgumentHandlerWrapper.java create mode 100644 spring-webflow/src/test/java/org/springframework/webflow/executor/support/FlowIdMappingArgumentHandlerWrapperTests.java diff --git a/spring-webflow/changelog.txt b/spring-webflow/changelog.txt index dd57bd03..f32c5126 100644 --- a/spring-webflow/changelog.txt +++ b/spring-webflow/changelog.txt @@ -64,6 +64,7 @@ Package org.springframework.webflow.executor FlowAction and FlowPhaseListener to use it. * JSF flow execution lifecycle now respects flow execution locking contract (SWF-229, SWF-276). * Fixed bug in FlowPhaseListener that resulted in a NPE on a flow that started and ended in the same request (SWF-273). +* Added FlowIdMappingArgumentHandlerWrapper (SWF-204). Package org.springframework.webflow.test * Added constructor taking test name argument to AbstractFlowExecutionTests, AbstractExternalizedFlowExecutionTests diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/support/FlowIdMappingArgumentHandlerWrapper.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/support/FlowIdMappingArgumentHandlerWrapper.java new file mode 100644 index 00000000..5eb21691 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/support/FlowIdMappingArgumentHandlerWrapper.java @@ -0,0 +1,186 @@ +/* + * 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. + */ +package org.springframework.webflow.executor.support; + +import java.util.Enumeration; +import java.util.Properties; + +import org.springframework.util.StringUtils; +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.execution.FlowExecutionContext; +import org.springframework.webflow.execution.support.ExternalRedirect; +import org.springframework.webflow.execution.support.FlowDefinitionRedirect; + +/** + * Flow executor argument handler that wraps another argument handler + * and applies a flow id mapping. This can be used to avoid literal flow ids in + * URLs that launch flows. + *

+ * For example, when used in combination with {@link RequestParameterFlowExecutorArgumentHandler} + * the url http://localhost/springair/reservation/booking.html would + * launch a new execution of the booking-flow flow, assuming a context path of + * /springair, a servlet mapping of /reservation/* and a flow id + * mapping of booking->booking-flow. + * + * @see RequestParameterFlowExecutorArgumentHandler + * @see RequestPathFlowExecutorArgumentHandler + * + * @author Andrej Zachar + * @author Erwin Vervaet + */ +public class FlowIdMappingArgumentHandlerWrapper extends FlowExecutorArgumentHandler { + + private FlowExecutorArgumentHandler argumentHandler; + private Properties mappings = new Properties(); + private Properties reverseMappings = new Properties(); + private boolean fallback = true; + + /** + * Default constructor for bean style usage. + * @see #setArgumentHandler(FlowExecutorArgumentHandler) + * @see #setMappings(Properties) + * @see #setFallback(boolean) + */ + public FlowIdMappingArgumentHandlerWrapper() { + } + + /** + * Returns the wrapped argument handler. + */ + public FlowExecutorArgumentHandler getArgumentHandler() { + return argumentHandler; + } + + /** + * Set the wrapped argument handler. + */ + public void setArgumentHandler(FlowExecutorArgumentHandler argumentHandler) { + this.argumentHandler = argumentHandler; + } + + /** + * Returns the flow id mappings in use. + */ + protected Properties getMappings() { + return mappings; + } + + /** + * Set the flow id mappings to use, overwriting any previous mappings. + */ + public void setMappings(Properties mappings) { + for (Enumeration fromFlowIds = mappings.propertyNames(); fromFlowIds.hasMoreElements(); ) { + String fromFlowId = (String)fromFlowIds.nextElement(); + String toFlowId = mappings.getProperty(fromFlowId); + addMapping(fromFlowId, toFlowId); + } + } + + /** + * Add a flow id mapping, overwriting any previous mapping for the same + * flow ids. + */ + public void addMapping(String fromFlowId, String toFlowId) { + mappings.setProperty(fromFlowId, toFlowId); + reverseMappings.setProperty(toFlowId, fromFlowId); + } + + /** + * Should we fall back to the flow id extracted by the wrapped argument + * handler if no mapping is defined for a flow id? Default is true. + */ + public boolean isFallback() { + return fallback; + } + + /** + * Set whether or not to fall back on the flow id extracted by the + * wrapped argument handler if no mapping is defined for a flow id. + * Default is true. When false an exception is thrown when there is + * a mapping failure. + */ + public void setFallback(boolean fallback) { + this.fallback = fallback; + } + + public boolean isFlowIdPresent(ExternalContext context) { + if (argumentHandler.isFlowIdPresent(context)) { + return fallback || mappings.containsKey(argumentHandler.extractFlowId(context)); + } + else { + return false; + } + } + + public String extractFlowId(ExternalContext context) throws FlowExecutorArgumentExtractionException { + String originalFlowId = argumentHandler.extractFlowId(context); + String flowId = mappings.getProperty(originalFlowId); + if (!StringUtils.hasText(flowId)) { + if (fallback) { + flowId = originalFlowId; + } + else { + throw new FlowExecutorArgumentExtractionException( + "Unable to extract flow definition id: no mapping was defined for flow id '" + + originalFlowId + "'"); + } + } + return flowId; + } + + public boolean isFlowExecutionKeyPresent(ExternalContext context) { + return argumentHandler.isFlowExecutionKeyPresent(context); + } + + public String extractFlowExecutionKey(ExternalContext context) throws FlowExecutorArgumentExtractionException { + return argumentHandler.extractFlowExecutionKey(context); + } + + public boolean isEventIdPresent(ExternalContext context) { + return argumentHandler.isEventIdPresent(context); + } + + public String extractEventId(ExternalContext context) throws FlowExecutorArgumentExtractionException { + return argumentHandler.extractEventId(context); + } + + public String createFlowDefinitionUrl(FlowDefinitionRedirect flowDefinitionRedirect, ExternalContext context) { + // do reverse mapping + String flowId = reverseMappings.getProperty(flowDefinitionRedirect.getFlowDefinitionId()); + if (!StringUtils.hasText(flowId)) { + if (fallback) { + flowId = flowDefinitionRedirect.getFlowDefinitionId(); + } + else { + // this is a mapping problem + throw new IllegalArgumentException( + "Unable to create a flow definition URL for '" + flowDefinitionRedirect + "': " + + "no reverse mapping was defined for flow id '" + flowDefinitionRedirect.getFlowDefinitionId() + "'"); + } + } + flowDefinitionRedirect = new FlowDefinitionRedirect(flowId, flowDefinitionRedirect.getExecutionInput()); + return argumentHandler.createFlowDefinitionUrl(flowDefinitionRedirect, context); + } + + public String createFlowExecutionUrl(String flowExecutionKey, + FlowExecutionContext flowExecution, ExternalContext context) { + return argumentHandler.createFlowExecutionUrl(flowExecutionKey, flowExecution, context); + } + + public String createExternalUrl(ExternalRedirect redirect, String flowExecutionKey, ExternalContext context) { + return argumentHandler.createExternalUrl(redirect, flowExecutionKey, context); + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/executor/support/FlowIdMappingArgumentHandlerWrapperTests.java b/spring-webflow/src/test/java/org/springframework/webflow/executor/support/FlowIdMappingArgumentHandlerWrapperTests.java new file mode 100644 index 00000000..7b93d02c --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/executor/support/FlowIdMappingArgumentHandlerWrapperTests.java @@ -0,0 +1,154 @@ +/* + * 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. + */ +package org.springframework.webflow.executor.support; + +import java.util.Collections; +import java.util.Properties; + +import junit.framework.TestCase; + +import org.springframework.util.StringUtils; +import org.springframework.webflow.execution.support.FlowDefinitionRedirect; +import org.springframework.webflow.test.MockExternalContext; + +/** + * Test case for {@link FlowIdMappingArgumentHandlerWrapper}. + * + * @author Erwin Vervaet + */ +public class FlowIdMappingArgumentHandlerWrapperTests extends TestCase { + + private FlowIdMappingArgumentHandlerWrapper argumentHandler; + + protected void setUp() throws Exception { + this.argumentHandler = new FlowIdMappingArgumentHandlerWrapper(); + this.argumentHandler.setArgumentHandler(new RequestPathFlowExecutorArgumentHandler()); + Properties mappings = new Properties(); + mappings.setProperty("A", "X"); + mappings.setProperty("B", "Y"); + argumentHandler.setMappings(mappings); + argumentHandler.addMapping("C", "X"); + } + + public void testMappingNoFallback() { + argumentHandler.setFallback(false); + + assertTrue(argumentHandler.isFlowIdPresent(context("A"))); + assertEquals("X", argumentHandler.extractFlowId(context("A"))); + assertTrue(argumentHandler.isFlowIdPresent(context("B"))); + assertEquals("Y", argumentHandler.extractFlowId(context("B"))); + assertTrue(argumentHandler.isFlowIdPresent(context("C"))); + assertEquals("X", argumentHandler.extractFlowId(context("C"))); + assertFalse(argumentHandler.isFlowIdPresent(context("X"))); + assertFalse(argumentHandler.isFlowIdPresent(context("Y"))); + try { + argumentHandler.extractFlowId(context("X")); + fail(); + } + catch (FlowExecutorArgumentExtractionException e) { + // expected + } + try { + argumentHandler.extractFlowId(context("")); + fail(); + } + catch (FlowExecutorArgumentExtractionException e) { + // expected + } + } + + public void testMappingFallback() { + argumentHandler.setFallback(true); + + assertTrue(argumentHandler.isFlowIdPresent(context("A"))); + assertEquals("X", argumentHandler.extractFlowId(context("A"))); + assertTrue(argumentHandler.isFlowIdPresent(context("B"))); + assertEquals("Y", argumentHandler.extractFlowId(context("B"))); + assertTrue(argumentHandler.isFlowIdPresent(context("C"))); + assertEquals("X", argumentHandler.extractFlowId(context("C"))); + assertTrue(argumentHandler.isFlowIdPresent(context("X"))); + assertEquals("X", argumentHandler.extractFlowId(context("X"))); + assertTrue(argumentHandler.isFlowIdPresent(context("Y"))); + assertEquals("Y", argumentHandler.extractFlowId(context("Y"))); + try { + argumentHandler.extractFlowId(context("")); + fail(); + } + catch (FlowExecutorArgumentExtractionException e) { + //expected + } + } + + public void testReverseMappingNoFallBack() { + argumentHandler.setFallback(false); + + assertEquals("/app/flows/C", argumentHandler.createFlowDefinitionUrl(redirect("X"), context())); + assertEquals("/app/flows/B", argumentHandler.createFlowDefinitionUrl(redirect("Y"), context())); + + try { + argumentHandler.createFlowDefinitionUrl(redirect("Z"), context()); + fail(); + } + catch (IllegalArgumentException e) { + // expected + } + } + + public void testReverseMappingFallback() { + argumentHandler.setFallback(true); + + assertEquals("/app/flows/C", argumentHandler.createFlowDefinitionUrl(redirect("X"), context())); + assertEquals("/app/flows/B", argumentHandler.createFlowDefinitionUrl(redirect("Y"), context())); + assertEquals("/app/flows/Z", argumentHandler.createFlowDefinitionUrl(redirect("Z"), context())); + } + + public void testWithRequestParameters() { + argumentHandler.setArgumentHandler(new RequestParameterFlowExecutorArgumentHandler()); + + // mapping + assertTrue(argumentHandler.isFlowIdPresent(contextWithParam("A"))); + assertEquals("X", argumentHandler.extractFlowId(contextWithParam("A"))); + + // reverse mapping + assertEquals("/app/flows?_flowId=C", argumentHandler.createFlowDefinitionUrl(redirect("X"), context())); + } + + // internal helpers + + private MockExternalContext context() { + return context(""); + } + + private MockExternalContext context(String flowId) { + MockExternalContext context = new MockExternalContext(); + context.setContextPath("/app"); + context.setDispatcherPath("/flows"); + if (StringUtils.hasText(flowId)) { + context.setRequestPathInfo("/" + flowId); + } + return context; + } + + private MockExternalContext contextWithParam(String flowId) { + MockExternalContext context = context(); + context.putRequestParameter("_flowId", flowId); + return context; + } + + private FlowDefinitionRedirect redirect(String flowId) { + return new FlowDefinitionRedirect(flowId, Collections.EMPTY_MAP); + } +}