View Javadoc

1   /*
2    * Copyright 2006-2008 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.springframework.batch.core.configuration.xml;
17  
18  import java.util.ArrayList;
19  import java.util.Arrays;
20  import java.util.Collection;
21  import java.util.HashMap;
22  import java.util.HashSet;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Set;
26  
27  import org.springframework.batch.core.job.flow.FlowExecutionStatus;
28  import org.springframework.beans.factory.config.BeanDefinition;
29  import org.springframework.beans.factory.parsing.CompositeComponentDefinition;
30  import org.springframework.beans.factory.support.BeanDefinitionBuilder;
31  import org.springframework.beans.factory.support.ManagedList;
32  import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
33  import org.springframework.beans.factory.xml.ParserContext;
34  import org.springframework.util.StringUtils;
35  import org.springframework.util.xml.DomUtils;
36  import org.w3c.dom.Element;
37  import org.w3c.dom.Node;
38  import org.w3c.dom.NodeList;
39  
40  /**
41   * @author Dave Syer
42   * 
43   */
44  public abstract class AbstractFlowParser extends AbstractSingleBeanDefinitionParser {
45  
46  	private static final String ID_ATTR = "id";
47  
48  	private static final String STEP_ELE = "step";
49  
50  	private static final String FLOW_ELE = "flow";
51  
52  	private static final String DECISION_ELE = "decision";
53  
54  	private static final String SPLIT_ELE = "split";
55  
56  	private static final String NEXT_ATTR = "next";
57  
58  	private static final String NEXT_ELE = "next";
59  
60  	private static final String END_ELE = "end";
61  
62  	private static final String FAIL_ELE = "fail";
63  
64  	private static final String STOP_ELE = "stop";
65  
66  	private static final String ON_ATTR = "on";
67  
68  	private static final String TO_ATTR = "to";
69  
70  	private static final String RESTART_ATTR = "restart";
71  
72  	private static final String EXIT_CODE_ATTR = "exit-code";
73  
74  	private static final InlineStepParser stepParser = new InlineStepParser();
75  
76  	private static final FlowElementParser flowParser = new FlowElementParser();
77  
78  	private static final DecisionParser decisionParser = new DecisionParser();
79  
80  	// For generating unique state names for end transitions
81  	private static int endCounter = 0;
82  
83  	private String jobFactoryRef;
84  
85  	/**
86  	 * Convenience method for subclasses to set the job factory reference if it
87  	 * is available (null is fine, but the quality of error reports is better if
88  	 * it is available).
89  	 * 
90  	 * @param jobFactoryRef
91  	 */
92  	protected void setJobFactoryRef(String jobFactoryRef) {
93  		this.jobFactoryRef = jobFactoryRef;
94  	}
95  
96  	/*
97  	 * (non-Javadoc)
98  	 * 
99  	 * @see AbstractSingleBeanDefinitionParser#getBeanClass(Element)
100 	 */
101 	@Override
102 	protected Class<?> getBeanClass(Element element) {
103 		return SimpleFlowFactoryBean.class;
104 	}
105 
106 	/**
107 	 * @param element the top level element containing a flow definition
108 	 * @param parserContext the {@link ParserContext}
109 	 */
110 	@Override
111 	protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
112 
113 		List<BeanDefinition> stateTransitions = new ArrayList<BeanDefinition>();
114 
115 		SplitParser splitParser = new SplitParser(jobFactoryRef);
116 		CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(),
117 				parserContext.extractSource(element));
118 		parserContext.pushContainingComponent(compositeDef);
119 
120 		boolean stepExists = false;
121 		Map<String, Set<String>> reachableElementMap = new HashMap<String, Set<String>>();
122 		String startElement = null;
123 		NodeList children = element.getChildNodes();
124 		for (int i = 0; i < children.getLength(); i++) {
125 			Node node = children.item(i);
126 			if (node instanceof Element) {
127 				String nodeName = node.getLocalName();
128 				Element child = (Element) node;
129 				if (nodeName.equals(STEP_ELE)) {
130 					stateTransitions.addAll(stepParser.parse(child, parserContext, jobFactoryRef));
131 					stepExists = true;
132 				}
133 				else if (nodeName.equals(DECISION_ELE)) {
134 					stateTransitions.addAll(decisionParser.parse(child, parserContext));
135 				}
136 				else if (nodeName.equals(FLOW_ELE)) {
137 					stateTransitions.addAll(flowParser.parse(child, parserContext));
138 					stepExists = true;
139 				}
140 				else if (nodeName.equals(SPLIT_ELE)) {
141 					stateTransitions.addAll(splitParser
142 							.parse(child, new ParserContext(parserContext.getReaderContext(), parserContext
143 									.getDelegate(), builder.getBeanDefinition())));
144 					stepExists = true;
145 				}
146 
147 				if (Arrays.asList(STEP_ELE, DECISION_ELE, SPLIT_ELE, FLOW_ELE).contains(nodeName)) {
148 					reachableElementMap.put(child.getAttribute(ID_ATTR), findReachableElements(child));
149 					if (startElement == null) {
150 						startElement = child.getAttribute(ID_ATTR);
151 					}
152 				}
153 			}
154 		}
155 
156 		String flowName = (String) builder.getRawBeanDefinition().getAttribute("flowName");
157 		if (!stepExists && !StringUtils.hasText(element.getAttribute("parent"))) {
158 			parserContext.getReaderContext().error("The flow [" + flowName + "] must contain at least one step, flow or split",
159 					element);
160 		}
161 
162 		// Ensure that all elements are reachable
163 		Set<String> allReachableElements = new HashSet<String>();
164 		findAllReachableElements(startElement, reachableElementMap, allReachableElements);
165 		for (String elementId : reachableElementMap.keySet()) {
166 			if (!allReachableElements.contains(elementId)) {
167 				parserContext.getReaderContext().error("The element [" + elementId + "] is unreachable", element);
168 			}
169 		}
170 
171 		ManagedList managedList = new ManagedList();
172 		@SuppressWarnings( { "unchecked", "unused" })
173 		boolean dummy = managedList.addAll(stateTransitions);
174 		builder.addPropertyValue("stateTransitions", managedList);
175 
176 	}
177 
178 	/**
179 	 * Find all of the elements that are pointed to by this element.
180 	 * 
181 	 * @param element
182 	 * @return a collection of reachable element names
183 	 */
184 	private Set<String> findReachableElements(Element element) {
185 		Set<String> reachableElements = new HashSet<String>();
186 
187 		String nextAttribute = element.getAttribute(NEXT_ATTR);
188 		if (StringUtils.hasText(nextAttribute)) {
189 			reachableElements.add(nextAttribute);
190 		}
191 
192 		List<Element> nextElements = DomUtils.getChildElementsByTagName(element, NEXT_ELE);
193 		for (Element nextElement : nextElements) {
194 			String toAttribute = nextElement.getAttribute(TO_ATTR);
195 			reachableElements.add(toAttribute);
196 		}
197 
198 		List<Element> stopElements = DomUtils.getChildElementsByTagName(element, STOP_ELE);
199 		for (Element stopElement : stopElements) {
200 			String restartAttribute = stopElement.getAttribute(RESTART_ATTR);
201 			reachableElements.add(restartAttribute);
202 		}
203 
204 		return reachableElements;
205 	}
206 
207 	/**
208 	 * Find all of the elements reachable from the startElement.
209 	 * 
210 	 * @param startElement
211 	 * @param reachableElementMap
212 	 * @param accumulator a collection of reachable element names
213 	 */
214 	private void findAllReachableElements(String startElement, Map<String, Set<String>> reachableElementMap,
215 			Set<String> accumulator) {
216 		Set<String> reachableIds = reachableElementMap.get(startElement);
217 		accumulator.add(startElement);
218 		if (reachableIds != null) {
219 			for (String reachable : reachableIds) {
220 				// don't explore a previously explored element; prevent loop
221 				if (!accumulator.contains(reachable)) {
222 					findAllReachableElements(reachable, reachableElementMap, accumulator);
223 				}
224 			}
225 		}
226 	}
227 
228 	/**
229 	 * @param parserContext the parser context for the bean factory
230 	 * @param stateDef The bean definition for the current state
231 	 * @param element the &lt;step/gt; element to parse
232 	 * @return a collection of
233 	 * {@link org.springframework.batch.core.job.flow.support.StateTransition}
234 	 * references
235 	 */
236 	protected static Collection<BeanDefinition> getNextElements(ParserContext parserContext, BeanDefinition stateDef,
237 			Element element) {
238 		return getNextElements(parserContext, null, stateDef, element);
239 	}
240 
241 	/**
242 	 * @param parserContext the parser context for the bean factory
243 	 * @param stepId the id of the current state if it is a step state, null
244 	 * otherwise
245 	 * @param stateDef The bean definition for the current state
246 	 * @param element the &lt;step/gt; element to parse
247 	 * @return a collection of
248 	 * {@link org.springframework.batch.core.job.flow.support.StateTransition}
249 	 * references
250 	 */
251 	protected static Collection<BeanDefinition> getNextElements(ParserContext parserContext, String stepId,
252 			BeanDefinition stateDef, Element element) {
253 
254 		Collection<BeanDefinition> list = new ArrayList<BeanDefinition>();
255 
256 		String shortNextAttribute = element.getAttribute(NEXT_ATTR);
257 		boolean hasNextAttribute = StringUtils.hasText(shortNextAttribute);
258 		if (hasNextAttribute) {
259 			list.add(getStateTransitionReference(parserContext, stateDef, null, shortNextAttribute));
260 		}
261 
262 		boolean transitionElementExists = false;
263 		List<String> patterns = new ArrayList<String>();
264 		for (String transitionName : new String[] { NEXT_ELE, STOP_ELE, END_ELE, FAIL_ELE }) {
265 			List<Element> transitionElements = DomUtils.getChildElementsByTagName(element, transitionName);
266 			for (Element transitionElement : transitionElements) {
267 				verifyUniquePattern(transitionElement, patterns, element, parserContext);
268 				list.addAll(parseTransitionElement(transitionElement, stepId, stateDef, parserContext));
269 				transitionElementExists = true;
270 			}
271 		}
272 
273 		if (!transitionElementExists) {
274 			list.addAll(createTransition(FlowExecutionStatus.FAILED, FlowExecutionStatus.FAILED.getName(), null, null,
275 					stateDef, parserContext, false));
276 			list.addAll(createTransition(FlowExecutionStatus.UNKNOWN, FlowExecutionStatus.UNKNOWN.getName(), null, null,
277 					stateDef, parserContext, false));
278 			if (!hasNextAttribute) {
279 				list.addAll(createTransition(FlowExecutionStatus.COMPLETED, null, null, null, stateDef, parserContext,
280 						false));
281 			}
282 		}
283 		else if (hasNextAttribute) {
284 			parserContext.getReaderContext().error(
285 					"The <" + element.getNodeName() + "/> may not contain a '" + NEXT_ATTR
286 							+ "' attribute and a transition element", element);
287 		}
288 
289 		return list;
290 	}
291 
292 	/**
293 	 * @param transitionElement The element to parse
294 	 * @param patterns a list of patterns on state transitions for this element
295 	 * @param element
296 	 * @param parserContext the parser context for the bean factory
297 	 */
298 	private static void verifyUniquePattern(Element transitionElement, List<String> patterns, Element element,
299 			ParserContext parserContext) {
300 		String onAttribute = transitionElement.getAttribute(ON_ATTR);
301 		if (patterns.contains(onAttribute)) {
302 			parserContext.getReaderContext().error("Duplicate transition pattern found for '" + onAttribute + "'",
303 					element);
304 		}
305 		patterns.add(onAttribute);
306 	}
307 
308 	/**
309 	 * @param transitionElement The element to parse
310 	 * @param stateDef The bean definition for the current state
311 	 * @param parserContext the parser context for the bean factory
312 	 * @param a collection of
313 	 * {@link org.springframework.batch.core.job.flow.support.StateTransition}
314 	 * references
315 	 */
316 	private static Collection<BeanDefinition> parseTransitionElement(Element transitionElement, String stateId,
317 			BeanDefinition stateDef, ParserContext parserContext) {
318 
319 		FlowExecutionStatus status = getBatchStatusFromEndTransitionName(transitionElement.getNodeName());
320 		String onAttribute = transitionElement.getAttribute(ON_ATTR);
321 		String restartAttribute = transitionElement.getAttribute(RESTART_ATTR);
322 		String nextAttribute = transitionElement.getAttribute(TO_ATTR);
323 		if (!StringUtils.hasText(nextAttribute)) {
324 			nextAttribute = restartAttribute;
325 		}
326 		boolean abandon = stateId != null && StringUtils.hasText(restartAttribute) && !restartAttribute.equals(stateId);
327 		String exitCodeAttribute = transitionElement.getAttribute(EXIT_CODE_ATTR);
328 
329 		return createTransition(status, onAttribute, nextAttribute, exitCodeAttribute, stateDef, parserContext, abandon);
330 	}
331 
332 	/**
333 	 * @param status The batch status that this transition will set. Use
334 	 * BatchStatus.UNKNOWN if not applicable.
335 	 * @param on The pattern that this transition should match. Use null for
336 	 * "no restriction" (same as "*").
337 	 * @param next The state to which this transition should go. Use null if not
338 	 * applicable.
339 	 * @param exitCode The exit code that this transition will set. Use null to
340 	 * default to batchStatus.
341 	 * @param stateDef The bean definition for the current state
342 	 * @param parserContext the parser context for the bean factory
343 	 * @param a collection of
344 	 * {@link org.springframework.batch.core.job.flow.support.StateTransition}
345 	 * references
346 	 */
347 	private static Collection<BeanDefinition> createTransition(FlowExecutionStatus status, String on, String next,
348 			String exitCode, BeanDefinition stateDef, ParserContext parserContext, boolean abandon) {
349 
350 		BeanDefinition endState = null;
351 
352 		if (status.isEnd()) {
353 
354 			BeanDefinitionBuilder endBuilder = BeanDefinitionBuilder
355 					.genericBeanDefinition("org.springframework.batch.core.job.flow.support.state.EndState");
356 
357 			boolean exitCodeExists = StringUtils.hasText(exitCode);
358 
359 			endBuilder.addConstructorArgValue(status);
360 
361 			endBuilder.addConstructorArgValue(exitCodeExists ? exitCode : status.getName());
362 
363 			String endName = (status == FlowExecutionStatus.STOPPED ? STOP_ELE
364 					: status == FlowExecutionStatus.FAILED ? FAIL_ELE : END_ELE)
365 					+ (endCounter++);
366 			endBuilder.addConstructorArgValue(endName);
367 
368 			endBuilder.addConstructorArgValue(abandon);
369 
370 			String nextOnEnd = exitCodeExists ? null : next;
371 			endState = getStateTransitionReference(parserContext, endBuilder.getBeanDefinition(), null, nextOnEnd);
372 			next = endName;
373 
374 		}
375 
376 		Collection<BeanDefinition> list = new ArrayList<BeanDefinition>();
377 		list.add(getStateTransitionReference(parserContext, stateDef, on, next));
378 		if (endState != null) {
379 			//
380 			// Must be added after the state to ensure that the state is the
381 			// first in the list
382 			//
383 			list.add(endState);
384 		}
385 		return list;
386 	}
387 
388 	/**
389 	 * @param elementName An end transition element name
390 	 * @return the BatchStatus corresponding to the transition name
391 	 */
392 	private static FlowExecutionStatus getBatchStatusFromEndTransitionName(String elementName) {
393         elementName = stripNamespace(elementName);
394 		if (STOP_ELE.equals(elementName)) {
395 			return FlowExecutionStatus.STOPPED;
396 		}
397 		else if (END_ELE.equals(elementName)) {
398 			return FlowExecutionStatus.COMPLETED;
399 		}
400 		else if (FAIL_ELE.equals(elementName)) {
401 			return FlowExecutionStatus.FAILED;
402 		}
403 		else {
404 			return FlowExecutionStatus.UNKNOWN;
405 		}
406 	}
407 
408     /**
409      * Strip the namespace from the element name if it exists.
410      */
411     private static String stripNamespace(String elementName){
412         if(elementName.startsWith("batch:")){
413             return elementName.substring(6);
414         }
415         else{
416             return elementName;
417         }
418     }
419 
420 	/**
421 	 * @param parserContext the parser context
422 	 * @param stateDefinition a reference to the state implementation
423 	 * @param on the pattern value
424 	 * @param next the next step id
425 	 * @return a bean definition for a
426 	 * {@link org.springframework.batch.core.job.flow.support.StateTransition}
427 	 */
428 	public static BeanDefinition getStateTransitionReference(ParserContext parserContext,
429 			BeanDefinition stateDefinition, String on, String next) {
430 
431 		BeanDefinitionBuilder nextBuilder = BeanDefinitionBuilder
432 				.genericBeanDefinition("org.springframework.batch.core.job.flow.support.StateTransition");
433 		nextBuilder.addConstructorArgValue(stateDefinition);
434 
435 		if (StringUtils.hasText(on)) {
436 			nextBuilder.addConstructorArgValue(on);
437 		}
438 
439 		if (StringUtils.hasText(next)) {
440 			nextBuilder.setFactoryMethod("createStateTransition");
441 			nextBuilder.addConstructorArgValue(next);
442 		}
443 		else {
444 			nextBuilder.setFactoryMethod("createEndStateTransition");
445 		}
446 
447 		return nextBuilder.getBeanDefinition();
448 
449 	}
450 
451 }