IN PROGRESS - issue SWF-545: Add support for merging a state in one flow model with a state in another

http://jira.springframework.org/browse/SWF-545
This commit is contained in:
Scott Andrews
2008-03-24 15:19:41 +00:00
parent c85e6f6320
commit dded1235b3
16 changed files with 282 additions and 18 deletions

View File

@@ -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
*/

View File

@@ -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));

View File

@@ -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()));
}

View File

@@ -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));

View File

@@ -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));

View File

@@ -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
*/

View File

@@ -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));

View File

@@ -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));

View File

@@ -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() {

View File

@@ -206,7 +206,7 @@ A transition out of this state is driven by the result of action execution.
</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:sequence>
<xsd:sequence minOccurs="0">
<xsd:element name="attribute" type="attribute" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation>
@@ -276,6 +276,19 @@ Handles exceptions that occur within this state.
<xsd:documentation>
<![CDATA[
The unique identifier of this state; must be unique to this flow.
]]>
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="parent" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
<![CDATA[
Flow identifier to inherit from for this state only. By default, the inherited flow's state identifer will match this state's identifier.
<p>
For example <pre>&lt;state id="state" parent="flow"&gt;</pre> defaults to <pre>&lt;state id="state" parent="flow#state"&gt;</pre>.
To inherit from a different identifier <pre>&lt;state id="state" parent="flow#other"&gt;</pre>
</ul>
]]>
</xsd:documentation>
</xsd:annotation>
@@ -428,6 +441,19 @@ Handles exceptions that occur within this state.
<xsd:documentation>
<![CDATA[
The unique identifier of this state; must be unique to this flow.
]]>
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="parent" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
<![CDATA[
Flow identifier to inherit from for this state only. By default, the inherited flow's state identifer will match this state's identifier.
<p>
For example <pre>&lt;state id="state" parent="flow"&gt;</pre> defaults to <pre>&lt;state id="state" parent="flow#state"&gt;</pre>.
To inherit from a different identifier <pre>&lt;state id="state" parent="flow#other"&gt;</pre>
</ul>
]]>
</xsd:documentation>
</xsd:annotation>
@@ -617,6 +643,19 @@ Handles exceptions that occur within this state.
<xsd:documentation>
<![CDATA[
The unique identifier of this state; must be unique to this flow.
]]>
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="parent" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
<![CDATA[
Flow identifier to inherit from for this state only. By default, the inherited flow's state identifer will match this state's identifier.
<p>
For example <pre>&lt;state id="state" parent="flow"&gt;</pre> defaults to <pre>&lt;state id="state" parent="flow#state"&gt;</pre>.
To inherit from a different identifier <pre>&lt;state id="state" parent="flow#other"&gt;</pre>
</ul>
]]>
</xsd:documentation>
</xsd:annotation>
@@ -636,7 +675,7 @@ A transition is triggered by the subflow outcome that was reached.
</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:sequence>
<xsd:sequence minOccurs="0" >
<xsd:element name="attribute" type="attribute" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation>
@@ -727,7 +766,20 @@ The unique identifier of this state; must be unique to this flow.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="subflow" type="flowId" use="required">
<xsd:attribute name="parent" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
<![CDATA[
Flow identifier to inherit from for this state only. By default, the inherited flow's state identifer will match this state's identifier.
<p>
For example <pre>&lt;state id="state" parent="flow"&gt;</pre> defaults to <pre>&lt;state id="state" parent="flow#state"&gt;</pre>.
To inherit from a different identifier <pre>&lt;state id="state" parent="flow#other"&gt;</pre>
</ul>
]]>
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="subflow" type="flowId">
<xsd:annotation>
<xsd:documentation>
<![CDATA[
@@ -818,6 +870,19 @@ Handles exceptions that occur in this state.
<xsd:documentation>
<![CDATA[
The unique identifier of this state; must be unique to this flow.
]]>
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="parent" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
<![CDATA[
Flow identifier to inherit from for this state only. By default, the inherited flow's state identifer will match this state's identifier.
<p>
For example <pre>&lt;state id="state" parent="flow"&gt;</pre> defaults to <pre>&lt;state id="state" parent="flow#state"&gt;</pre>.
To inherit from a different identifier <pre>&lt;state id="state" parent="flow#other"&gt;</pre>
</ul>
]]>
</xsd:documentation>
</xsd:annotation>

View File

@@ -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
}
}
}

View File

@@ -0,0 +1,5 @@
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
</flow>

View File

@@ -0,0 +1,7 @@
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
<action-state id="view" parent="parent" />
</flow>

View File

@@ -0,0 +1,7 @@
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
<view-state id="view" parent="parent" />
</flow>

View File

@@ -0,0 +1,7 @@
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
<view-state id="view" parent="parent#other" />
</flow>

View File

@@ -0,0 +1,8 @@
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
<view-state id="view" view="mainview"/>
<view-state id="other" view="otherview"/>
</flow>