/* * Copyright 2004-2015 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.engine.model.builder.xml; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import javax.xml.parsers.ParserConfigurationException; import org.springframework.core.io.Resource; import org.springframework.core.style.ToStringCreator; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; import org.springframework.webflow.engine.model.AbstractActionModel; import org.springframework.webflow.engine.model.AbstractStateModel; import org.springframework.webflow.engine.model.ActionStateModel; import org.springframework.webflow.engine.model.AttributeModel; import org.springframework.webflow.engine.model.BeanImportModel; import org.springframework.webflow.engine.model.BinderModel; import org.springframework.webflow.engine.model.BindingModel; import org.springframework.webflow.engine.model.DecisionStateModel; import org.springframework.webflow.engine.model.EndStateModel; import org.springframework.webflow.engine.model.EvaluateModel; import org.springframework.webflow.engine.model.ExceptionHandlerModel; import org.springframework.webflow.engine.model.FlowModel; import org.springframework.webflow.engine.model.IfModel; import org.springframework.webflow.engine.model.InputModel; import org.springframework.webflow.engine.model.OutputModel; import org.springframework.webflow.engine.model.PersistenceContextModel; import org.springframework.webflow.engine.model.RenderModel; import org.springframework.webflow.engine.model.SecuredModel; import org.springframework.webflow.engine.model.SetModel; import org.springframework.webflow.engine.model.SubflowStateModel; import org.springframework.webflow.engine.model.TransitionModel; import org.springframework.webflow.engine.model.VarModel; import org.springframework.webflow.engine.model.ViewStateModel; import org.springframework.webflow.engine.model.builder.FlowModelBuilder; import org.springframework.webflow.engine.model.builder.FlowModelBuilderException; import org.springframework.webflow.engine.model.registry.FlowModelHolder; import org.springframework.webflow.engine.model.registry.FlowModelHolderLocator; import org.springframework.webflow.engine.model.registry.FlowModelLocator; import org.springframework.webflow.engine.model.registry.NoSuchFlowModelException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.xml.sax.SAXException; /** * Builds a flow model from a XML-based flow definition resource. * * @author Keith Donald * @author Scott Andrews * @author Rossen Stoyanchev */ public class XmlFlowModelBuilder implements FlowModelBuilder { private Resource resource; private FlowModelLocator modelLocator; private DocumentLoader documentLoader = new DefaultDocumentLoader(); private Document document; private long lastModifiedTimestamp; private FlowModel flowModel; private final List<FlowModelHolder> parentHolders = new ArrayList<FlowModelHolder>(4); /** * Create a new XML flow model builder that will parse the XML document at the specified resource location and use * the provided locator to access parent flow models. * @param resource the path to the XML flow definition (required) */ public XmlFlowModelBuilder(Resource resource) { init(resource, null); } /** * Create a new XML flow model builder that will parse the XML document at the specified resource location and use * the provided locator to access parent flow models. * @param resource the path to the XML flow definition (required) * @param modelLocator a locator for parent flow models to support flow inheritance */ public XmlFlowModelBuilder(Resource resource, FlowModelLocator modelLocator) { init(resource, modelLocator); } /** * Sets the loader that will load the XML-based flow definition document. Optional, defaults to * {@link DefaultDocumentLoader}. * @param documentLoader the document loader */ public void setDocumentLoader(DocumentLoader documentLoader) { Assert.notNull(documentLoader, "The XML document loader is required"); this.documentLoader = documentLoader; } public void init() throws FlowModelBuilderException { try { document = documentLoader.loadDocument(resource); initLastModifiedTimestamp(); } catch (IOException e) { throw new FlowModelBuilderException("Could not access the XML flow definition at " + resource, e); } catch (ParserConfigurationException e) { throw new FlowModelBuilderException("Could not configure the parser to parse the XML flow definition at " + resource, e); } catch (SAXException e) { throw new FlowModelBuilderException("Could not parse the XML flow definition document at " + resource, e); } } public void build() throws FlowModelBuilderException { if (getDocumentElement() == null) { throw new FlowModelBuilderException( "The FlowModelBuilder must be initialized first -- called init() before calling build()"); } flowModel = parseFlow(getDocumentElement()); mergeFlows(); mergeStates(); } public FlowModel getFlowModel() throws FlowModelBuilderException { if (flowModel == null) { throw new FlowModelBuilderException( "The FlowModel must be built first -- called init() and build() before calling getFlowModel()"); } return flowModel; } public void dispose() throws FlowModelBuilderException { document = null; flowModel = null; } public Resource getFlowModelResource() { return resource; } public boolean hasFlowModelResourceChanged() { if (lastModifiedTimestamp == -1) { return false; } try { long lastModified = resource.lastModified(); if (lastModified > lastModifiedTimestamp) { return true; } else { for (FlowModelHolder parent : this.parentHolders) { if (parent.hasFlowModelChanged()) { return true; } } return false; } } catch (IOException e) { return false; } } /** * Returns the DOM document parsed from the XML file. */ protected Document getDocument() { return document; } /** * Returns the root document element. */ protected Element getDocumentElement() { return document != null ? document.getDocumentElement() : null; } private void init(Resource resource, FlowModelLocator modelLocator) { Assert.notNull(resource, "The location of the XML-based flow definition is required"); this.resource = resource; this.modelLocator = modelLocator; } private void initLastModifiedTimestamp() { try { lastModifiedTimestamp = resource.lastModified(); } catch (IOException e) { lastModifiedTimestamp = -1; } } private FlowModel parseFlow(Element element) { FlowModel flow = new FlowModel(); flow.setAbstract(element.getAttribute("abstract")); flow.setParent(element.getAttribute("parent")); flow.setStartStateId(element.getAttribute("start-state")); flow.setAttributes(parseAttributes(element)); flow.setSecured(parseSecured(element)); flow.setPersistenceContext(parsePersistenceContext(element)); flow.setVars(parseVars(element)); flow.setInputs(parseInputs(element)); flow.setOnStartActions(parseOnStartActions(element)); flow.setStates(parseStates(element)); flow.setGlobalTransitions(parseGlobalTransitions(element)); flow.setOnEndActions(parseOnEndActions(element)); flow.setOutputs(parseOutputs(element)); flow.setExceptionHandlers(parseExceptionHandlers(element)); flow.setBeanImports(parseBeanImports(element)); return flow; } private LinkedList<AttributeModel> parseAttributes(Element element) { List<Element> attributeElements = DomUtils.getChildElementsByTagName(element, "attribute"); if (attributeElements.isEmpty()) { return null; } LinkedList<AttributeModel> attributes = new LinkedList<AttributeModel>(); for (Element element2 : attributeElements) { attributes.add(parseAttribute(element2)); } return attributes; } private LinkedList<VarModel> parseVars(Element element) { List<Element> varElements = DomUtils.getChildElementsByTagName(element, "var"); if (varElements.isEmpty()) { return null; } LinkedList<VarModel> vars = new LinkedList<VarModel>(); for (Element element2 : varElements) { vars.add(parseVar(element2)); } return vars; } private LinkedList<InputModel> parseInputs(Element element) { List<Element> inputElements = DomUtils.getChildElementsByTagName(element, "input"); if (inputElements.isEmpty()) { return null; } LinkedList<InputModel> inputs = new LinkedList<InputModel>(); for (Element element2 : inputElements) { inputs.add(parseInput(element2)); } return inputs; } private LinkedList<OutputModel> parseOutputs(Element element) { List<Element> outputElements = DomUtils.getChildElementsByTagName(element, "output"); if (outputElements.isEmpty()) { return null; } LinkedList<OutputModel> outputs = new LinkedList<OutputModel>(); for (Element element2 : outputElements) { outputs.add(parseOutput(element2)); } return outputs; } private LinkedList<AbstractActionModel> parseActions(Element element) { List<Element> actionElements = DomUtils.getChildElementsByTagName(element, new String[] { "evaluate", "render", "set" }); if (actionElements.isEmpty()) { return null; } LinkedList<AbstractActionModel> actions = new LinkedList<AbstractActionModel>(); for (Element element2 : actionElements) { actions.add(parseAction(element2)); } return actions; } private LinkedList<AbstractStateModel> parseStates(Element element) { List<Element> stateElements = DomUtils.getChildElementsByTagName(element, new String[] { "view-state", "action-state", "decision-state", "subflow-state", "end-state" }); if (stateElements.isEmpty()) { return null; } LinkedList<AbstractStateModel> states = new LinkedList<AbstractStateModel>(); for (Element element2 : stateElements) { states.add(parseState(element2)); } return states; } private LinkedList<TransitionModel> parseTransitions(Element element) { List<Element> transitionElements = DomUtils.getChildElementsByTagName(element, "transition"); if (transitionElements.isEmpty()) { return null; } LinkedList<TransitionModel> transitions = new LinkedList<TransitionModel>(); for (Element element2 : transitionElements) { transitions.add(parseTransition(element2)); } return transitions; } private LinkedList<ExceptionHandlerModel> parseExceptionHandlers(Element element) { List<Element> exceptionHandlerElements = DomUtils.getChildElementsByTagName(element, "exception-handler"); if (exceptionHandlerElements.isEmpty()) { return null; } LinkedList<ExceptionHandlerModel> exceptionHandlers = new LinkedList<ExceptionHandlerModel>(); for (Element element2 : exceptionHandlerElements) { exceptionHandlers.add(parseExceptionHandler(element2)); } return exceptionHandlers; } private LinkedList<BeanImportModel> parseBeanImports(Element element) { List<Element> importElements = DomUtils.getChildElementsByTagName(element, "bean-import"); if (importElements.isEmpty()) { return null; } LinkedList<BeanImportModel> beanImports = new LinkedList<BeanImportModel>(); for (Element element2 : importElements) { beanImports.add(parseBeanImport(element2)); } return beanImports; } private LinkedList<IfModel> parseIfs(Element element) { List<Element> ifElements = DomUtils.getChildElementsByTagName(element, "if"); if (ifElements.isEmpty()) { return null; } LinkedList<IfModel> ifs = new LinkedList<IfModel>(); for (Element element2 : ifElements) { ifs.add(parseIf(element2)); } return ifs; } private AbstractActionModel parseAction(Element element) { if (DomUtils.nodeNameEquals(element, "evaluate")) { return parseEvaluate(element); } else if (DomUtils.nodeNameEquals(element, "render")) { return parseRender(element); } else if (DomUtils.nodeNameEquals(element, "set")) { return parseSet(element); } else { throw new FlowModelBuilderException("Unknown action element encountered '" + element.getLocalName() + "'"); } } private AbstractStateModel parseState(Element element) { if (DomUtils.nodeNameEquals(element, "view-state")) { return parseViewState(element); } else if (DomUtils.nodeNameEquals(element, "action-state")) { return parseActionState(element); } else if (DomUtils.nodeNameEquals(element, "decision-state")) { return parseDecisionState(element); } else if (DomUtils.nodeNameEquals(element, "subflow-state")) { return parseSubflowState(element); } else if (DomUtils.nodeNameEquals(element, "end-state")) { return parseEndState(element); } else { throw new FlowModelBuilderException("Unknown state element encountered '" + element.getLocalName() + "'"); } } private LinkedList<TransitionModel> parseGlobalTransitions(Element element) { element = DomUtils.getChildElementByTagName(element, "global-transitions"); if (element == null) { return null; } else { return parseTransitions(element); } } private AttributeModel parseAttribute(Element element) { AttributeModel attribute = new AttributeModel(element.getAttribute("name"), parseAttributeValue(element)); attribute.setType(element.getAttribute("type")); return attribute; } private String parseAttributeValue(Element element) { if (element.hasAttribute("value")) { return element.getAttribute("value"); } else { Element valueElement = DomUtils.getChildElementByTagName(element, "value"); if (valueElement != null) { return DomUtils.getTextValue(valueElement); } else { return null; } } } private SecuredModel parseSecured(Element element) { element = DomUtils.getChildElementByTagName(element, "secured"); if (element == null) { return null; } else { SecuredModel secured = new SecuredModel(element.getAttribute("attributes")); secured.setMatch(element.getAttribute("match")); return secured; } } private PersistenceContextModel parsePersistenceContext(Element element) { element = DomUtils.getChildElementByTagName(element, "persistence-context"); if (element == null) { return null; } else { return new PersistenceContextModel(); } } private VarModel parseVar(Element element) { return new VarModel(element.getAttribute("name"), element.getAttribute("class")); } private InputModel parseInput(Element element) { InputModel input = new InputModel(element.getAttribute("name"), element.getAttribute("value")); input.setType(element.getAttribute("type")); input.setRequired(element.getAttribute("required")); return input; } private OutputModel parseOutput(Element element) { OutputModel output = new OutputModel(element.getAttribute("name"), element.getAttribute("value")); output.setType(element.getAttribute("type")); output.setRequired(element.getAttribute("required")); return output; } private TransitionModel parseTransition(Element element) { TransitionModel transition = new TransitionModel(); transition.setOn(element.getAttribute("on")); transition.setTo(element.getAttribute("to")); transition.setOnException(element.getAttribute("on-exception")); transition.setBind(element.getAttribute("bind")); transition.setValidate(element.getAttribute("validate")); transition.setValidationHints(element.getAttribute("validation-hints")); transition.setHistory(element.getAttribute("history")); transition.setAttributes(parseAttributes(element)); transition.setSecured(parseSecured(element)); transition.setActions(parseActions(element)); return transition; } private ExceptionHandlerModel parseExceptionHandler(Element element) { return new ExceptionHandlerModel(element.getAttribute("bean")); } private BeanImportModel parseBeanImport(Element element) { return new BeanImportModel(element.getAttribute("resource")); } private IfModel parseIf(Element element) { IfModel ifModel = new IfModel(element.getAttribute("test"), element.getAttribute("then")); ifModel.setElse(element.getAttribute("else")); return ifModel; } private LinkedList<AbstractActionModel> parseOnStartActions(Element element) { Element onStartElement = DomUtils.getChildElementByTagName(element, "on-start"); if (onStartElement != null) { return parseActions(onStartElement); } else { return null; } } private LinkedList<AbstractActionModel> parseOnEntryActions(Element element) { Element onEntryElement = DomUtils.getChildElementByTagName(element, "on-entry"); if (onEntryElement != null) { return parseActions(onEntryElement); } else { return null; } } private LinkedList<AbstractActionModel> parseOnRenderActions(Element element) { Element onRenderElement = DomUtils.getChildElementByTagName(element, "on-render"); if (onRenderElement != null) { return parseActions(onRenderElement); } else { return null; } } private BinderModel parseBinder(Element element) { Element binderElement = DomUtils.getChildElementByTagName(element, "binder"); if (binderElement != null) { BinderModel binder = new BinderModel(); binder.setBindings(parseBindings(binderElement)); return binder; } else { return null; } } private LinkedList<BindingModel> parseBindings(Element element) { List<Element> bindingElements = DomUtils.getChildElementsByTagName(element, "binding"); if (bindingElements.isEmpty()) { return null; } LinkedList<BindingModel> bindings = new LinkedList<BindingModel>(); for (Element element2 : bindingElements) { bindings.add(parseBinding(element2)); } return bindings; } private BindingModel parseBinding(Element element) { return new BindingModel(element.getAttribute("property"), element.getAttribute("converter"), element.getAttribute("required")); } private LinkedList<AbstractActionModel> parseOnExitActions(Element element) { Element onExitElement = DomUtils.getChildElementByTagName(element, "on-exit"); if (onExitElement != null) { return parseActions(onExitElement); } else { return null; } } private LinkedList<AbstractActionModel> parseOnEndActions(Element element) { Element onEndElement = DomUtils.getChildElementByTagName(element, "on-end"); if (onEndElement != null) { return parseActions(onEndElement); } else { return null; } } private EvaluateModel parseEvaluate(Element element) { EvaluateModel evaluate = new EvaluateModel(element.getAttribute("expression")); evaluate.setResult(element.getAttribute("result")); evaluate.setResultType(element.getAttribute("result-type")); evaluate.setAttributes(parseAttributes(element)); return evaluate; } private RenderModel parseRender(Element element) { RenderModel render = new RenderModel(element.getAttribute("fragments")); render.setAttributes(parseAttributes(element)); return render; } private SetModel parseSet(Element element) { SetModel set = new SetModel(element.getAttribute("name"), element.getAttribute("value")); set.setType(element.getAttribute("type")); set.setAttributes(parseAttributes(element)); return set; } 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)); state.setTransitions(parseTransitions(element)); state.setOnExitActions(parseOnExitActions(element)); state.setActions(parseActions(element)); state.setExceptionHandlers(parseExceptionHandlers(element)); return state; } 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")); state.setModel(element.getAttribute("model")); state.setValidationHints(element.getAttribute("validation-hints")); state.setVars(parseVars(element)); state.setBinder(parseBinder(element)); state.setOnRenderActions(parseOnRenderActions(element)); state.setAttributes(parseAttributes(element)); state.setSecured(parseSecured(element)); state.setOnEntryActions(parseOnEntryActions(element)); state.setExceptionHandlers(parseExceptionHandlers(element)); state.setTransitions(parseTransitions(element)); state.setOnExitActions(parseOnExitActions(element)); return state; } 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)); state.setSecured(parseSecured(element)); state.setOnEntryActions(parseOnEntryActions(element)); state.setExceptionHandlers(parseExceptionHandlers(element)); return state; } 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)); state.setAttributes(parseAttributes(element)); state.setSecured(parseSecured(element)); state.setOnEntryActions(parseOnEntryActions(element)); state.setExceptionHandlers(parseExceptionHandlers(element)); state.setTransitions(parseTransitions(element)); state.setOnExitActions(parseOnExitActions(element)); return state; } 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)); state.setAttributes(parseAttributes(element)); state.setSecured(parseSecured(element)); state.setOnEntryActions(parseOnEntryActions(element)); state.setExceptionHandlers(parseExceptionHandlers(element)); return state; } private void mergeFlows() { if (flowModel.getParent() != null) { List<String> parents = Arrays.asList(StringUtils.trimArrayElements(flowModel.getParent().split(","))); for (String parentFlowId : parents) { if (StringUtils.hasText(parentFlowId)) { try { flowModel.merge(modelLocator.getFlowModel(parentFlowId)); } catch (NoSuchFlowModelException e) { throw new FlowModelBuilderException("Unable to find flow '" + parentFlowId + "' to inherit from", e); } try { if (this.modelLocator instanceof FlowModelHolderLocator) { FlowModelHolderLocator locator = (FlowModelHolderLocator) this.modelLocator; this.parentHolders.add(locator.getFlowModelHolder(parentFlowId)); } } catch (NoSuchFlowModelException e) { // Ignore } } } } } private void mergeStates() { if (flowModel.getStates() == null) { return; } for (AbstractStateModel childState : flowModel.getStates()) { String parent = childState.getParent(); if (childState.getParent() != null) { String flowId; String stateId; AbstractStateModel parentState = null; int hashIndex = parent.indexOf("#"); if (hashIndex == -1) { throw new FlowModelBuilderException("Invalid parent syntax '" + parent + "', should take form 'flowId#stateId'"); } flowId = parent.substring(0, hashIndex).trim(); stateId = parent.substring(hashIndex + 1).trim(); try { if (StringUtils.hasText(flowId)) { parentState = modelLocator.getFlowModel(flowId).getStateById(stateId); } else { parentState = flowModel.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 String toString() { return new ToStringCreator(this).append("resource", resource).toString(); } }