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