diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/AbstractStateModel.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/AbstractStateModel.java index d2e2f262..27d8601b 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/AbstractStateModel.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/AbstractStateModel.java @@ -26,6 +26,7 @@ import org.springframework.util.StringUtils; */ public abstract class AbstractStateModel extends AbstractModel { private String id; + private String parent; private LinkedList attributes; private SecuredModel secured; private LinkedList onEntryActions; @@ -49,6 +50,24 @@ public abstract class AbstractStateModel extends AbstractModel { } } + /** + * @return the parent + */ + public String getParent() { + return parent; + } + + /** + * @param parent the parent to set + */ + public void setParent(String parent) { + if (StringUtils.hasText(parent)) { + this.parent = parent; + } else { + this.parent = null; + } + } + /** * @return the attributes */ diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/ActionStateModel.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/ActionStateModel.java index 5545a6e5..fd88a7a9 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/ActionStateModel.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/ActionStateModel.java @@ -44,6 +44,7 @@ public class ActionStateModel extends AbstractTransitionableStateModel { public void merge(Model model) { ActionStateModel state = (ActionStateModel) model; + setParent(null); setAttributes(merge(getAttributes(), state.getAttributes())); setSecured((SecuredModel) merge(getSecured(), state.getSecured())); setOnEntryActions(merge(getOnEntryActions(), state.getOnEntryActions(), false)); diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/AttributeModel.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/AttributeModel.java index 439841c4..2505d1c3 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/AttributeModel.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/AttributeModel.java @@ -45,12 +45,12 @@ public class AttributeModel extends AbstractModel { return false; } AttributeModel attribute = (AttributeModel) model; - return ObjectUtils.nullSafeEquals(getName(), attribute.getName()) - && ObjectUtils.nullSafeEquals(getValue(), attribute.getValue()); + return ObjectUtils.nullSafeEquals(getName(), attribute.getName()); } public void merge(Model model) { AttributeModel attribute = (AttributeModel) model; + setValue(merge(getValue(), attribute.getValue())); setType(merge(getType(), attribute.getType())); } diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/DecisionStateModel.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/DecisionStateModel.java index 57bd5c82..09cbe3ea 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/DecisionStateModel.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/DecisionStateModel.java @@ -45,6 +45,7 @@ public class DecisionStateModel extends AbstractStateModel { public void merge(Model model) { DecisionStateModel state = (DecisionStateModel) model; + setParent(null); setAttributes(merge(getAttributes(), state.getAttributes())); setSecured((SecuredModel) merge(getSecured(), state.getSecured())); setOnEntryActions(merge(getOnEntryActions(), state.getOnEntryActions(), false)); diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/EndStateModel.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/EndStateModel.java index 83ddd2d4..b5ad0203 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/EndStateModel.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/EndStateModel.java @@ -47,6 +47,7 @@ public class EndStateModel extends AbstractStateModel { public void merge(Model model) { EndStateModel state = (EndStateModel) model; + setParent(null); setAttributes(merge(getAttributes(), state.getAttributes())); setSecured((SecuredModel) merge(getSecured(), state.getSecured())); setOnEntryActions(merge(getOnEntryActions(), state.getOnEntryActions(), false)); diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/FlowModel.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/FlowModel.java index 014329b5..23430000 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/FlowModel.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/FlowModel.java @@ -15,6 +15,7 @@ */ package org.springframework.webflow.engine.model; +import java.util.Iterator; import java.util.LinkedList; import org.springframework.util.StringUtils; @@ -371,6 +372,23 @@ public class FlowModel extends AbstractModel { return states; } + /** + * Get the state model for an identifier + * @param id the state identifier to find + * @return the state or null if the identifier was not found + */ + public AbstractStateModel getStateById(String id) { + if (states != null) { + for (Iterator it = states.iterator(); it.hasNext();) { + AbstractStateModel state = (AbstractStateModel) it.next(); + if (id.equals(state.getId())) { + return state; + } + } + } + return null; + } + /** * @param states the states to set */ diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/SubflowStateModel.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/SubflowStateModel.java index 2d7a807c..7fd9df1c 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/SubflowStateModel.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/SubflowStateModel.java @@ -50,6 +50,7 @@ public class SubflowStateModel extends AbstractTransitionableStateModel { public void merge(Model model) { SubflowStateModel state = (SubflowStateModel) model; + setParent(null); setAttributes(merge(getAttributes(), state.getAttributes())); setSecured((SecuredModel) merge(getSecured(), state.getSecured())); setOnEntryActions(merge(getOnEntryActions(), state.getOnEntryActions(), false)); diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/ViewStateModel.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/ViewStateModel.java index cdb84fbc..cbf01861 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/ViewStateModel.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/ViewStateModel.java @@ -51,6 +51,7 @@ public class ViewStateModel extends AbstractTransitionableStateModel { public void merge(Model model) { ViewStateModel state = (ViewStateModel) model; + setParent(null); setAttributes(merge(getAttributes(), state.getAttributes())); setSecured((SecuredModel) merge(getSecured(), state.getSecured())); setOnEntryActions(merge(getOnEntryActions(), state.getOnEntryActions(), false)); diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/builder/xml/XmlFlowModelBuilder.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/builder/xml/XmlFlowModelBuilder.java index 89861f84..78ea672a 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/builder/xml/XmlFlowModelBuilder.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/builder/xml/XmlFlowModelBuilder.java @@ -18,6 +18,7 @@ package org.springframework.webflow.engine.model.builder.xml; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; import java.util.List; @@ -121,6 +122,11 @@ public class XmlFlowModelBuilder implements FlowModelBuilder { "The FlowModelBuilder must be initialized first -- called init() before calling build()"); } flowModel = parseFlow(getDocumentElement()); + mergeFlows(); + mergeStates(); + } + + protected void mergeFlows() { if (flowModel.getParent() != null) { List parents = Arrays.asList(StringUtils.trimArrayElements(flowModel.getParent().split(","))); for (Iterator it = parents.iterator(); it.hasNext();) { @@ -137,6 +143,42 @@ public class XmlFlowModelBuilder implements FlowModelBuilder { } } + protected void mergeStates() { + if (flowModel.getStates() == null) { + return; + } + for (Iterator it = flowModel.getStates().iterator(); it.hasNext();) { + AbstractStateModel childState = (AbstractStateModel) it.next(); + String parent = childState.getParent(); + if (childState.getParent() != null) { + String flowId; + String stateId; + AbstractStateModel parentState = null; + if (parent.contains("#")) { + flowId = parent.substring(0, parent.indexOf("#")).trim(); + stateId = parent.substring(parent.indexOf("#") + 1).trim(); + } else { + flowId = parent.trim(); + stateId = childState.getId(); + } + try { + parentState = modelLocator.getFlowModel(flowId).getStateById(stateId); + if (parentState == null) { + throw new FlowModelBuilderException("Unable to find state '" + stateId + "' in flow '" + flowId + + "'"); + } + childState.merge(parentState); + } catch (NoSuchFlowModelException e) { + throw new FlowModelBuilderException("Unable to find flow '" + flowId + "' to inherit from", e); + } catch (ClassCastException e) { + throw new FlowModelBuilderException("Parent state type '" + parentState.getClass().getName() + + "' cannot be merged with state type '" + childState.getClass().getName() + "'", e); + + } + } + } + } + public FlowModel getFlowModel() throws FlowModelBuilderException { return flowModel; } @@ -245,7 +287,8 @@ public class XmlFlowModelBuilder implements FlowModelBuilder { } private LinkedList parseActions(Element element) { - List actionElements = getChildElementsByTagNames(element, new String[] { "evaluate", "render", "set" }); + List actionElements = getChildElementsByTagNames(element, Arrays.asList(new String[] { "evaluate", "render", + "set" })); if (actionElements.isEmpty()) { return null; } @@ -257,8 +300,8 @@ public class XmlFlowModelBuilder implements FlowModelBuilder { } private LinkedList parseStates(Element element) { - List stateElements = getChildElementsByTagNames(element, new String[] { "view-state", "action-state", - "decision-state", "subflow-state", "end-state" }); + List stateElements = getChildElementsByTagNames(element, Arrays.asList(new String[] { "view-state", + "action-state", "decision-state", "subflow-state", "end-state" })); if (stateElements.isEmpty()) { return null; } @@ -503,6 +546,7 @@ public class XmlFlowModelBuilder implements FlowModelBuilder { private ActionStateModel parseActionState(Element element) { ActionStateModel state = new ActionStateModel(element.getAttribute("id")); + state.setParent(element.getAttribute("parent")); state.setAttributes(parseAttributes(element)); state.setSecured(parseSecured(element)); state.setOnEntryActions(parseOnEntryActions(element)); @@ -515,6 +559,7 @@ public class XmlFlowModelBuilder implements FlowModelBuilder { private ViewStateModel parseViewState(Element element) { ViewStateModel state = new ViewStateModel(element.getAttribute("id")); + state.setParent(element.getAttribute("parent")); state.setView(element.getAttribute("view")); state.setRedirect(element.getAttribute("redirect")); state.setPopup(element.getAttribute("popup")); @@ -532,6 +577,7 @@ public class XmlFlowModelBuilder implements FlowModelBuilder { private DecisionStateModel parseDecisionState(Element element) { DecisionStateModel state = new DecisionStateModel(element.getAttribute("id")); + state.setParent(element.getAttribute("parent")); state.setIfs(parseIfs(element)); state.setOnExitActions(parseOnExitActions(element)); state.setAttributes(parseAttributes(element)); @@ -543,6 +589,7 @@ public class XmlFlowModelBuilder implements FlowModelBuilder { private SubflowStateModel parseSubflowState(Element element) { SubflowStateModel state = new SubflowStateModel(element.getAttribute("id"), element.getAttribute("subflow")); + state.setParent(element.getAttribute("parent")); state.setSubflowAttributeMapper(element.getAttribute("subflow-attribute-mapper")); state.setInputs(parseInputs(element)); state.setOutputs(parseOutputs(element)); @@ -557,6 +604,7 @@ public class XmlFlowModelBuilder implements FlowModelBuilder { private EndStateModel parseEndState(Element element) { EndStateModel state = new EndStateModel(element.getAttribute("id")); + state.setParent(element.getAttribute("parent")); state.setView(element.getAttribute("view")); state.setCommit(element.getAttribute("commit")); state.setOutputs(parseOutputs(element)); @@ -567,18 +615,23 @@ public class XmlFlowModelBuilder implements FlowModelBuilder { return state; } - // TODO: submit this to DomUtils in spring-core then remove here - private static List getChildElementsByTagNames(Element element, String[] childElementNames) { - List names = Arrays.asList(childElementNames); - NodeList nodeList = element.getChildNodes(); - List childElements = new ArrayList(); - for (int i = 0; i < nodeList.getLength(); i++) { - Node node = nodeList.item(i); - if (node instanceof Element && (names.contains(node.getLocalName()) || names.contains(node.getNodeName()))) { - childElements.add(node); + // TODO: submited to DomUtils in spring-core will be available in 2.5.3 + public static List getChildElementsByTagNames(Element ele, Collection childEleNames) { + Assert.notNull(ele, "Element must not be null"); + Assert.notNull(childEleNames, "Element names collection must not be null"); + NodeList nl = ele.getChildNodes(); + List childEles = new ArrayList(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (node instanceof Element && nodeNameMatch(node, childEleNames)) { + childEles.add(node); } } - return childElements; + return childEles; + } + + private static boolean nodeNameMatch(Node node, Collection desiredNames) { + return (desiredNames.contains(node.getNodeName()) || desiredNames.contains(node.getLocalName())); } public String toString() { diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/builder/xml/spring-webflow-2.0.xsd b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/builder/xml/spring-webflow-2.0.xsd index c8d24074..b39b9dc5 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/engine/model/builder/xml/spring-webflow-2.0.xsd +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/model/builder/xml/spring-webflow-2.0.xsd @@ -206,7 +206,7 @@ A transition out of this state is driven by the result of action execution. - + @@ -276,6 +276,19 @@ Handles exceptions that occur within this state. + + + + + + + +For example
<state id="state" parent="flow">
defaults to
<state id="state" parent="flow#state">
. +To inherit from a different identifier
<state id="state" parent="flow#other">
+ ]]>
@@ -428,6 +441,19 @@ Handles exceptions that occur within this state. + + +
+ + + + +For example
<state id="state" parent="flow">
defaults to
<state id="state" parent="flow#state">
. +To inherit from a different identifier
<state id="state" parent="flow#other">
+ ]]>
@@ -617,6 +643,19 @@ Handles exceptions that occur within this state. + + +
+ + + + +For example
<state id="state" parent="flow">
defaults to
<state id="state" parent="flow#state">
. +To inherit from a different identifier
<state id="state" parent="flow#other">
+ ]]>
@@ -636,7 +675,7 @@ A transition is triggered by the subflow outcome that was reached. - + @@ -727,7 +766,20 @@ The unique identifier of this state; must be unique to this flow.
- + + + + +For example
<state id="state" parent="flow">
defaults to
<state id="state" parent="flow#state">
. +To inherit from a different identifier
<state id="state" parent="flow#other">
+ +]]> +
+
+
+ + + + + + + + +For example
<state id="state" parent="flow">
defaults to
<state id="state" parent="flow#state">
. +To inherit from a different identifier
<state id="state" parent="flow#other">
+ ]]>
diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/XmlFlowModelBuilderTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/XmlFlowModelBuilderTests.java index 63847cb1..a890b3ef 100644 --- a/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/XmlFlowModelBuilderTests.java +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/XmlFlowModelBuilderTests.java @@ -172,6 +172,7 @@ public class XmlFlowModelBuilderTests extends TestCase { fail("A FlowModelConstructionException was expected"); } catch (FlowModelConstructionException e) { // we want this + e.printStackTrace(); } } @@ -184,4 +185,73 @@ public class XmlFlowModelBuilderTests extends TestCase { assertEquals(4, flow.getOnStartActions().size()); } + public void testStateMerge() { + ClassPathResource resourceChild = new ClassPathResource("flow-inheritance-state-child.xml", getClass()); + ClassPathResource resourceParent = new ClassPathResource("flow-inheritance-state-parent.xml", getClass()); + registry + .registerFlowModel(new DefaultFlowModelHolder(new XmlFlowModelBuilder(resourceChild, registry), "child")); + registry.registerFlowModel(new DefaultFlowModelHolder(new XmlFlowModelBuilder(resourceParent, registry), + "parent")); + FlowModel flow = registry.getFlowModel("child"); + assertEquals(1, flow.getStates().size()); + assertEquals("otherview", ((ViewStateModel) flow.getStates().get(0)).getView()); + } + + public void testStateMergeDefault() { + ClassPathResource resourceChild = new ClassPathResource("flow-inheritance-state-child-default.xml", getClass()); + ClassPathResource resourceParent = new ClassPathResource("flow-inheritance-state-parent.xml", getClass()); + registry + .registerFlowModel(new DefaultFlowModelHolder(new XmlFlowModelBuilder(resourceChild, registry), "child")); + registry.registerFlowModel(new DefaultFlowModelHolder(new XmlFlowModelBuilder(resourceParent, registry), + "parent")); + FlowModel flow = registry.getFlowModel("child"); + assertEquals(1, flow.getStates().size()); + assertEquals("mainview", ((ViewStateModel) flow.getStates().get(0)).getView()); + } + + public void testStateMergeParentFlowNotFound() { + ClassPathResource resourceChild = new ClassPathResource("flow-inheritance-state-child.xml", getClass()); + ClassPathResource resourceParent = new ClassPathResource("flow-inheritance-state-parent.xml", getClass()); + registry + .registerFlowModel(new DefaultFlowModelHolder(new XmlFlowModelBuilder(resourceChild, registry), "child")); + registry.registerFlowModel(new DefaultFlowModelHolder(new XmlFlowModelBuilder(resourceParent, registry), + "parent-id-not-matching")); + try { + registry.getFlowModel("child"); + fail("A FlowModelConstructionException was expected"); + } catch (FlowModelConstructionException e) { + // we want this + } + } + + public void testStateMergeParentStateNotFound() { + ClassPathResource resourceChild = new ClassPathResource("flow-inheritance-state-child.xml", getClass()); + ClassPathResource resourceParent = new ClassPathResource("flow-empty.xml", getClass()); + registry + .registerFlowModel(new DefaultFlowModelHolder(new XmlFlowModelBuilder(resourceChild, registry), "child")); + registry.registerFlowModel(new DefaultFlowModelHolder(new XmlFlowModelBuilder(resourceParent, registry), + "parent")); + try { + registry.getFlowModel("child"); + fail("A FlowModelConstructionException was expected"); + } catch (FlowModelConstructionException e) { + // we want this + } + } + + public void testStateMergeParentStateIncompatable() { + ClassPathResource resourceChild = new ClassPathResource("flow-inheritance-state-child-alt.xml", getClass()); + ClassPathResource resourceParent = new ClassPathResource("flow-inheritance-state-parent.xml", getClass()); + registry + .registerFlowModel(new DefaultFlowModelHolder(new XmlFlowModelBuilder(resourceChild, registry), "child")); + registry.registerFlowModel(new DefaultFlowModelHolder(new XmlFlowModelBuilder(resourceParent, registry), + "parent")); + try { + registry.getFlowModel("child"); + fail("A FlowModelConstructionException was expected"); + } catch (FlowModelConstructionException e) { + // we want this + } + } + } diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/flow-empty.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/flow-empty.xml new file mode 100644 index 00000000..cb39f147 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/flow-empty.xml @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/flow-inheritance-state-child-alt.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/flow-inheritance-state-child-alt.xml new file mode 100644 index 00000000..b9b10c0c --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/flow-inheritance-state-child-alt.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/flow-inheritance-state-child-default.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/flow-inheritance-state-child-default.xml new file mode 100644 index 00000000..3c407a37 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/flow-inheritance-state-child-default.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/flow-inheritance-state-child.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/flow-inheritance-state-child.xml new file mode 100644 index 00000000..fe147880 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/flow-inheritance-state-child.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/flow-inheritance-state-parent.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/flow-inheritance-state-parent.xml new file mode 100644 index 00000000..6225c36a --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/model/builder/xml/flow-inheritance-state-parent.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file