001/*
002 * Copyright 2006-2014 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.springframework.batch.core.configuration.xml;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.HashSet;
022import java.util.LinkedHashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026
027import org.w3c.dom.Element;
028import org.w3c.dom.Node;
029import org.w3c.dom.NodeList;
030
031import org.springframework.batch.core.job.flow.FlowExecutionStatus;
032import org.springframework.beans.factory.config.BeanDefinition;
033import org.springframework.beans.factory.parsing.CompositeComponentDefinition;
034import org.springframework.beans.factory.support.BeanDefinitionBuilder;
035import org.springframework.beans.factory.support.ManagedList;
036import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
037import org.springframework.beans.factory.xml.ParserContext;
038import org.springframework.util.StringUtils;
039import org.springframework.util.xml.DomUtils;
040
041/**
042 * @author Dave Syer
043 * @author Michael Minella
044 * @author Chris Schaefer
045 *
046 */
047public abstract class AbstractFlowParser extends AbstractSingleBeanDefinitionParser {
048
049        protected static final String ID_ATTR = "id";
050
051        protected static final String STEP_ELE = "step";
052
053        protected static final String FLOW_ELE = "flow";
054
055        protected static final String DECISION_ELE = "decision";
056
057        protected static final String SPLIT_ELE = "split";
058
059        protected static final String NEXT_ATTR = "next";
060
061        protected static final String NEXT_ELE = "next";
062
063        protected static final String END_ELE = "end";
064
065        protected static final String FAIL_ELE = "fail";
066
067        protected static final String STOP_ELE = "stop";
068
069        protected static final String ON_ATTR = "on";
070
071        protected static final String TO_ATTR = "to";
072
073        protected static final String RESTART_ATTR = "restart";
074
075        protected static final String EXIT_CODE_ATTR = "exit-code";
076
077        private static final InlineStepParser stepParser = new InlineStepParser();
078
079        private static final FlowElementParser flowParser = new FlowElementParser();
080
081        private static final DecisionParser decisionParser = new DecisionParser();
082
083        // For generating unique state names for end transitions
084        protected static int endCounter = 0;
085
086        private String jobFactoryRef;
087
088        /**
089         * Convenience method for subclasses to set the job factory reference if it
090         * is available (null is fine, but the quality of error reports is better if
091         * it is available).
092         *
093         * @param jobFactoryRef name of the ref
094         */
095        protected void setJobFactoryRef(String jobFactoryRef) {
096                this.jobFactoryRef = jobFactoryRef;
097        }
098
099        /*
100         * (non-Javadoc)
101         *
102         * @see AbstractSingleBeanDefinitionParser#getBeanClass(Element)
103         */
104        @Override
105        protected Class<?> getBeanClass(Element element) {
106                return SimpleFlowFactoryBean.class;
107        }
108
109        /**
110         * @param element the top level element containing a flow definition
111         * @param parserContext the {@link ParserContext}
112         */
113        @Override
114        protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
115
116                List<BeanDefinition> stateTransitions = new ArrayList<BeanDefinition>();
117
118                SplitParser splitParser = new SplitParser(jobFactoryRef);
119                CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(),
120                                parserContext.extractSource(element));
121                parserContext.pushContainingComponent(compositeDef);
122
123                boolean stepExists = false;
124                Map<String, Set<String>> reachableElementMap = new LinkedHashMap<String, Set<String>>();
125                String startElement = null;
126                NodeList children = element.getChildNodes();
127                for (int i = 0; i < children.getLength(); i++) {
128                        Node node = children.item(i);
129                        if (node instanceof Element) {
130                                String nodeName = node.getLocalName();
131                                Element child = (Element) node;
132                                if (nodeName.equals(STEP_ELE)) {
133                                        stateTransitions.addAll(stepParser.parse(child, parserContext, jobFactoryRef));
134                                        stepExists = true;
135                                }
136                                else if (nodeName.equals(DECISION_ELE)) {
137                                        stateTransitions.addAll(decisionParser.parse(child, parserContext));
138                                }
139                                else if (nodeName.equals(FLOW_ELE)) {
140                                        stateTransitions.addAll(flowParser.parse(child, parserContext));
141                                        stepExists = true;
142                                }
143                                else if (nodeName.equals(SPLIT_ELE)) {
144                                        stateTransitions.addAll(splitParser
145                                                        .parse(child, new ParserContext(parserContext.getReaderContext(), parserContext
146                                                                        .getDelegate(), builder.getBeanDefinition())));
147                                        stepExists = true;
148                                }
149
150                                if (Arrays.asList(STEP_ELE, DECISION_ELE, SPLIT_ELE, FLOW_ELE).contains(nodeName)) {
151                                        reachableElementMap.put(child.getAttribute(ID_ATTR), findReachableElements(child));
152                                        if (startElement == null) {
153                                                startElement = child.getAttribute(ID_ATTR);
154                                        }
155                                }
156                        }
157                }
158
159                String flowName = (String) builder.getRawBeanDefinition().getAttribute("flowName");
160                if (!stepExists && !StringUtils.hasText(element.getAttribute("parent"))) {
161                        parserContext.getReaderContext().error("The flow [" + flowName + "] must contain at least one step, flow or split",
162                                        element);
163                }
164
165                // Ensure that all elements are reachable
166                Set<String> allReachableElements = new HashSet<String>();
167                findAllReachableElements(startElement, reachableElementMap, allReachableElements);
168                for (String elementId : reachableElementMap.keySet()) {
169                        if (!allReachableElements.contains(elementId)) {
170                                parserContext.getReaderContext().error("The element [" + elementId + "] is unreachable", element);
171                        }
172                }
173
174                ManagedList<BeanDefinition> managedList = new ManagedList<BeanDefinition>();
175                managedList.addAll(stateTransitions);
176                builder.addPropertyValue("stateTransitions", managedList);
177
178        }
179
180        /**
181         * Find all of the elements that are pointed to by this element.
182         *
183         * @param element The parent element
184         * @return a collection of reachable element names
185         */
186        private Set<String> findReachableElements(Element element) {
187                Set<String> reachableElements = new HashSet<String>();
188
189                String nextAttribute = element.getAttribute(NEXT_ATTR);
190                if (StringUtils.hasText(nextAttribute)) {
191                        reachableElements.add(nextAttribute);
192                }
193
194                List<Element> nextElements = DomUtils.getChildElementsByTagName(element, NEXT_ELE);
195                for (Element nextElement : nextElements) {
196                        String toAttribute = nextElement.getAttribute(TO_ATTR);
197                        reachableElements.add(toAttribute);
198                }
199
200                List<Element> stopElements = DomUtils.getChildElementsByTagName(element, STOP_ELE);
201                for (Element stopElement : stopElements) {
202                        String restartAttribute = stopElement.getAttribute(RESTART_ATTR);
203                        reachableElements.add(restartAttribute);
204                }
205
206                return reachableElements;
207        }
208
209        /**
210         * Find all of the elements reachable from the startElement.
211         *
212         * @param startElement name of the element to start from
213         * @param reachableElementMap Map of elements that can be reached from the startElement
214         * @param accumulator a collection of reachable element names
215         */
216        protected void findAllReachableElements(String startElement, Map<String, Set<String>> reachableElementMap,
217                        Set<String> accumulator) {
218                Set<String> reachableIds = reachableElementMap.get(startElement);
219                accumulator.add(startElement);
220                if (reachableIds != null) {
221                        for (String reachable : reachableIds) {
222                                // don't explore a previously explored element; prevent loop
223                                if (!accumulator.contains(reachable)) {
224                                        findAllReachableElements(reachable, reachableElementMap, accumulator);
225                                }
226                        }
227                }
228        }
229
230        /**
231         * @param parserContext the parser context for the bean factory
232         * @param stateDef The bean definition for the current state
233         * @param element the &lt;step/gt; element to parse
234         * @return a collection of
235         * {@link org.springframework.batch.core.job.flow.support.StateTransition}
236         * references
237         */
238        public static Collection<BeanDefinition> getNextElements(ParserContext parserContext, BeanDefinition stateDef,
239                        Element element) {
240                return getNextElements(parserContext, null, stateDef, element);
241        }
242
243        /**
244         * @param parserContext the parser context for the bean factory
245         * @param stepId the id of the current state if it is a step state, null
246         * otherwise
247         * @param stateDef The bean definition for the current state
248         * @param element the &lt;step/gt; element to parse
249         * @return a collection of
250         * {@link org.springframework.batch.core.job.flow.support.StateTransition}
251         * references
252         */
253        public static Collection<BeanDefinition> getNextElements(ParserContext parserContext, String stepId,
254                        BeanDefinition stateDef, Element element) {
255
256                Collection<BeanDefinition> list = new ArrayList<BeanDefinition>();
257
258                String shortNextAttribute = element.getAttribute(NEXT_ATTR);
259                boolean hasNextAttribute = StringUtils.hasText(shortNextAttribute);
260                if (hasNextAttribute) {
261                        list.add(getStateTransitionReference(parserContext, stateDef, null, shortNextAttribute));
262                }
263
264                boolean transitionElementExists = false;
265                List<String> patterns = new ArrayList<String>();
266                for (String transitionName : new String[] { NEXT_ELE, STOP_ELE, END_ELE, FAIL_ELE }) {
267                        List<Element> transitionElements = DomUtils.getChildElementsByTagName(element, transitionName);
268                        for (Element transitionElement : transitionElements) {
269                                verifyUniquePattern(transitionElement, patterns, element, parserContext);
270                                list.addAll(parseTransitionElement(transitionElement, stepId, stateDef, parserContext));
271                                transitionElementExists = true;
272                        }
273                }
274
275                if (!transitionElementExists) {
276                        list.addAll(createTransition(FlowExecutionStatus.FAILED, FlowExecutionStatus.FAILED.getName(), null, null,
277                                        stateDef, parserContext, false));
278                        list.addAll(createTransition(FlowExecutionStatus.UNKNOWN, FlowExecutionStatus.UNKNOWN.getName(), null, null,
279                                        stateDef, parserContext, false));
280                        if (!hasNextAttribute) {
281                                list.addAll(createTransition(FlowExecutionStatus.COMPLETED, null, null, null, stateDef, parserContext,
282                                                false));
283                        }
284                }
285                else if (hasNextAttribute) {
286                        parserContext.getReaderContext().error(
287                                        "The <" + element.getNodeName() + "/> may not contain a '" + NEXT_ATTR
288                                        + "' attribute and a transition element", element);
289                }
290
291                return list;
292        }
293
294        /**
295         * @param transitionElement The element to parse
296         * @param patterns a list of patterns on state transitions for this element
297         * @param element {@link Element} representing the source.
298         * @param parserContext the parser context for the bean factory
299         */
300        protected static void verifyUniquePattern(Element transitionElement, List<String> patterns, Element element,
301                        ParserContext parserContext) {
302                String onAttribute = transitionElement.getAttribute(ON_ATTR);
303                if (patterns.contains(onAttribute)) {
304                        parserContext.getReaderContext().error("Duplicate transition pattern found for '" + onAttribute + "'",
305                                        element);
306                }
307                patterns.add(onAttribute);
308        }
309
310        /**
311         * @param transitionElement The element to parse
312         * @param stateDef The bean definition for the current state
313         * @param parserContext the parser context for the bean factory
314         * @return a collection of
315         * {@link org.springframework.batch.core.job.flow.support.StateTransition}
316         * references
317         */
318        private static Collection<BeanDefinition> parseTransitionElement(Element transitionElement, String stateId,
319                        BeanDefinition stateDef, ParserContext parserContext) {
320
321                FlowExecutionStatus status = getBatchStatusFromEndTransitionName(transitionElement.getNodeName());
322                String onAttribute = transitionElement.getAttribute(ON_ATTR);
323                String restartAttribute = transitionElement.getAttribute(RESTART_ATTR);
324                String nextAttribute = transitionElement.getAttribute(TO_ATTR);
325                if (!StringUtils.hasText(nextAttribute)) {
326                        nextAttribute = restartAttribute;
327                }
328                boolean abandon = stateId != null && StringUtils.hasText(restartAttribute) && !restartAttribute.equals(stateId);
329                String exitCodeAttribute = transitionElement.getAttribute(EXIT_CODE_ATTR);
330
331                return createTransition(status, onAttribute, nextAttribute, exitCodeAttribute, stateDef, parserContext, abandon);
332        }
333
334        /**
335         * @param status The batch status that this transition will set. Use
336         * BatchStatus.UNKNOWN if not applicable.
337         * @param on The pattern that this transition should match. Use null for
338         * "no restriction" (same as "*").
339         * @param next The state to which this transition should go. Use null if not
340         * applicable.
341         * @param exitCode The exit code that this transition will set. Use null to
342         * default to batchStatus.
343         * @param stateDef The bean definition for the current state
344         * @param parserContext the parser context for the bean factory
345         * @param abandon the abandon flag to be used by the transition.
346         * @return a collection of
347         * {@link org.springframework.batch.core.job.flow.support.StateTransition}
348         * references
349         */
350        protected static Collection<BeanDefinition> createTransition(FlowExecutionStatus status, String on, String next,
351                        String exitCode, BeanDefinition stateDef, ParserContext parserContext, boolean abandon) {
352
353                BeanDefinition endState = null;
354
355                if (status.isEnd()) {
356
357                        BeanDefinitionBuilder endBuilder = BeanDefinitionBuilder
358                                        .genericBeanDefinition("org.springframework.batch.core.job.flow.support.state.EndState");
359
360                        boolean exitCodeExists = StringUtils.hasText(exitCode);
361
362                        endBuilder.addConstructorArgValue(status);
363
364                        endBuilder.addConstructorArgValue(exitCodeExists ? exitCode : status.getName());
365
366                        String endName = (status == FlowExecutionStatus.STOPPED ? STOP_ELE
367                                        : status == FlowExecutionStatus.FAILED ? FAIL_ELE : END_ELE)
368                                        + (endCounter++);
369                        endBuilder.addConstructorArgValue(endName);
370
371                        endBuilder.addConstructorArgValue(abandon);
372
373                        String nextOnEnd = exitCodeExists ? null : next;
374                        endState = getStateTransitionReference(parserContext, endBuilder.getBeanDefinition(), null, nextOnEnd);
375                        next = endName;
376
377                }
378
379                Collection<BeanDefinition> list = new ArrayList<BeanDefinition>();
380                list.add(getStateTransitionReference(parserContext, stateDef, on, next));
381                if (endState != null) {
382                        //
383                        // Must be added after the state to ensure that the state is the
384                        // first in the list
385                        //
386                        list.add(endState);
387                }
388                return list;
389        }
390
391        /**
392         * @param elementName An end transition element name
393         * @return the BatchStatus corresponding to the transition name
394         */
395        protected static FlowExecutionStatus getBatchStatusFromEndTransitionName(String elementName) {
396                elementName = stripNamespace(elementName);
397                if (STOP_ELE.equals(elementName)) {
398                        return FlowExecutionStatus.STOPPED;
399                }
400                else if (END_ELE.equals(elementName)) {
401                        return FlowExecutionStatus.COMPLETED;
402                }
403                else if (FAIL_ELE.equals(elementName)) {
404                        return FlowExecutionStatus.FAILED;
405                }
406                else {
407                        return FlowExecutionStatus.UNKNOWN;
408                }
409        }
410
411        /**
412         * Strip the namespace from the element name if it exists.
413         */
414        private static String stripNamespace(String elementName){
415                if(elementName.startsWith("batch:")){
416                        return elementName.substring(6);
417                }
418                else{
419                        return elementName;
420                }
421        }
422
423        /**
424         * @param parserContext the parser context
425         * @param stateDefinition a reference to the state implementation
426         * @param on the pattern value
427         * @param next the next step id
428         * @return a bean definition for a
429         * {@link org.springframework.batch.core.job.flow.support.StateTransition}
430         */
431        public static BeanDefinition getStateTransitionReference(ParserContext parserContext,
432                        BeanDefinition stateDefinition, String on, String next) {
433
434                BeanDefinitionBuilder nextBuilder = BeanDefinitionBuilder
435                                .genericBeanDefinition("org.springframework.batch.core.job.flow.support.StateTransition");
436                nextBuilder.addConstructorArgValue(stateDefinition);
437
438                if (StringUtils.hasText(on)) {
439                        nextBuilder.addConstructorArgValue(on);
440                }
441
442                if (StringUtils.hasText(next)) {
443                        nextBuilder.setFactoryMethod("createStateTransition");
444                        nextBuilder.addConstructorArgValue(next);
445                }
446                else {
447                        nextBuilder.setFactoryMethod("createEndStateTransition");
448                }
449
450                return nextBuilder.getBeanDefinition();
451
452        }
453
454}