From e68d9a246a6276ea1910a4b6a0bd2fc2c60a4552 Mon Sep 17 00:00:00 2001 From: trisberg Date: Tue, 27 Jan 2009 02:20:06 +0000 Subject: [PATCH] BATCH-1004: parser now uses the step name for bean references to step bean definitions --- .../core/configuration/xml/StepParser.java | 1130 ++++++++--------- .../configuration/xml/StepParserTests.java | 13 +- .../xml/StepParserBeanNameTests-context.xml | 26 + 3 files changed, 601 insertions(+), 568 deletions(-) create mode 100644 spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StepParserBeanNameTests-context.xml diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/StepParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/StepParser.java index e9dbbc1eb..a382144d0 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/StepParser.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/StepParser.java @@ -1,567 +1,563 @@ -/* - * Copyright 2006-2009 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.batch.core.configuration.xml; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.BeanReference; -import org.springframework.beans.factory.config.RuntimeBeanReference; -import org.springframework.beans.factory.parsing.BeanComponentDefinition; -import org.springframework.beans.factory.support.AbstractBeanDefinition; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.support.ManagedList; -import org.springframework.beans.factory.support.ManagedMap; -import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.util.StringUtils; -import org.springframework.util.xml.DomUtils; - -import org.w3c.dom.Element; -import org.w3c.dom.NamedNodeMap; - -/** - * Internal parser for the <step/> elements inside a job. A step element - * references a bean definition for a {@link org.springframework.batch.core.Step} and goes on to (optionally) - * list a set of transitions from that step to others with <next on="pattern" - * to="stepName"/>. Used by the {@link JobParser}. - * - * @see JobParser - * - * @author Dave Syer - * @author Thomas Risberg - * @since 2.0 - */ -public class StepParser { - - // For generating unique state names for end transitions - private static int endCounter = 0; - - /** - * Parse the step and turn it into a list of transitions. - * - * @param element the <step/gt; element to parse - * @param parserContext the parser context for the bean factory - * @return a collection of bean definitions for {@link org.springframework.batch.core.job.flow.support.StateTransition} - * instances objects - */ - public Collection parse(Element element, ParserContext parserContext) { - - BeanDefinitionBuilder stateBuilder = - BeanDefinitionBuilder.genericBeanDefinition("org.springframework.batch.core.job.flow.support.state.StepState"); - String stepRef = element.getAttribute("name"); - String taskletRef = element.getAttribute("tasklet"); - - @SuppressWarnings("unchecked") - List processTaskElements = (List) DomUtils.getChildElementsByTagName(element, "tasklet"); - if (StringUtils.hasText(taskletRef)) { - Object task = handleTaskletRef(element, taskletRef, parserContext); - stateBuilder.addConstructorArgValue(stepRef); - stateBuilder.addConstructorArgValue(task); - } - else if (processTaskElements.size() > 0) { - Object task = handleTaskletElement(element, processTaskElements.get(0), parserContext); - stateBuilder.addConstructorArgValue(stepRef); - stateBuilder.addConstructorArgValue(task); - } - else if (StringUtils.hasText(stepRef)) { - RuntimeBeanReference stateDef = new RuntimeBeanReference(stepRef); - stateBuilder.addConstructorArgValue(stateDef); - } - else { - parserContext.getReaderContext().error("Incomplete configuration detected while creating step with name " + stepRef, element); - } - return getNextElements(parserContext, stateBuilder.getBeanDefinition(), element); - - } - - /** - * @param parserContext - * @param stateDef - * @param element - * @return a collection of {@link org.springframework.batch.core.job.flow.support.StateTransition} references - */ - public static Collection getNextElements(ParserContext parserContext, - BeanDefinition stateDef, Element element) { - - Collection list = new ArrayList(); - - String shortNextAttribute = element.getAttribute("next"); - boolean hasNextAttribute = StringUtils.hasText(shortNextAttribute); - if (hasNextAttribute) { - list.add(getStateTransitionReference(parserContext, stateDef, null, shortNextAttribute)); - } - - @SuppressWarnings("unchecked") - List nextElements = (List) DomUtils.getChildElementsByTagName(element, "next"); - @SuppressWarnings("unchecked") - List stopElements = (List) DomUtils.getChildElementsByTagName(element, "stop"); - nextElements.addAll(stopElements); - @SuppressWarnings("unchecked") - List endElements = (List) DomUtils.getChildElementsByTagName(element, "end"); - nextElements.addAll(endElements); - - for (Element nextElement : nextElements) { - String onAttribute = nextElement.getAttribute("on"); - String nextAttribute = nextElement.getAttribute("to"); - if (hasNextAttribute && onAttribute.equals("*")) { - parserContext.getReaderContext().error("Duplicate transition pattern found for '*' " - + "(only specify one of next= attribute at step level and next element with on='*')", - element); - } - - String name = nextElement.getNodeName(); - if ("stop".equals(name) || "end".equals(name)) { - - String statusName = nextElement.getAttribute("status"); - String status = StringUtils.hasText(statusName) ? statusName : "STOPPED"; - String nextOnEnd = StringUtils.hasText(statusName) ? null : nextAttribute; - - BeanDefinitionBuilder endBuilder = - BeanDefinitionBuilder.genericBeanDefinition("org.springframework.batch.core.job.flow.support.state.EndState"); - endBuilder.addConstructorArgValue(status); - String endName = "stop".equals(name) ? "end" + (endCounter++) : null; - - endBuilder.addConstructorArgValue(endName); - list.add(getStateTransitionReference(parserContext, endBuilder.getBeanDefinition(), onAttribute, nextOnEnd)); - nextAttribute = endName; - - } - list.add(getStateTransitionReference(parserContext, stateDef, onAttribute, nextAttribute)); - } - - if(hasNextAttribute && nextElements.isEmpty()) - { - list.add(getStateTransitionReference(parserContext, stateDef, "FAILED", null)); - } - - if (list.isEmpty() && !hasNextAttribute) { - list.add(getStateTransitionReference(parserContext, stateDef, null, null)); - } - - return list; - } - - /** - * @param parserContext the parser context - * @param stateDefinition a reference to the state implementation - * @param on the pattern value - * @param next the next step id - * @return a bean definition for a {@link org.springframework.batch.core.job.flow.support.StateTransition} - */ - public static RuntimeBeanReference getStateTransitionReference(ParserContext parserContext, - BeanDefinition stateDefinition, String on, String next) { - - BeanDefinitionBuilder nextBuilder = - BeanDefinitionBuilder.genericBeanDefinition("org.springframework.batch.core.job.flow.support.StateTransition"); - nextBuilder.addConstructorArgValue(stateDefinition); - - if (StringUtils.hasText(on)) { - nextBuilder.addConstructorArgValue(on); - } - - if (StringUtils.hasText(next)) { - nextBuilder.setFactoryMethod("createStateTransition"); - nextBuilder.addConstructorArgValue(next); - } - else { - nextBuilder.setFactoryMethod("createEndStateTransition"); - } - - // TODO: do we need to use RuntimeBeanReference? - AbstractBeanDefinition nextDef = nextBuilder.getBeanDefinition(); - String nextDefName = parserContext.getReaderContext().generateBeanName(nextDef); - BeanComponentDefinition nextDefComponent = new BeanComponentDefinition(nextDef, nextDefName); - parserContext.registerBeanComponent(nextDefComponent); - - return new RuntimeBeanReference(nextDefName); - - } - - /** - * @param stepElement - * @param taskletRef - * @param parserContext - * @return the TaskletStep bean - */ - protected RootBeanDefinition handleTaskletRef(Element stepElement, String taskletRef, ParserContext parserContext) { - - RootBeanDefinition bd = new RootBeanDefinition("org.springframework.batch.core.step.tasklet.TaskletStep", null, null); - - if (StringUtils.hasText(taskletRef)) { - RuntimeBeanReference taskletBeanRef = new RuntimeBeanReference(taskletRef); - bd.getPropertyValues().addPropertyValue("tasklet", taskletBeanRef); - } - - String jobRepositoryRef = stepElement.getAttribute("job-repository"); - RuntimeBeanReference jobRepositoryBeanRef = new RuntimeBeanReference(jobRepositoryRef); - bd.getPropertyValues().addPropertyValue("jobRepository", jobRepositoryBeanRef); - - String transactionManagerRef = stepElement.getAttribute("transaction-manager"); - RuntimeBeanReference transactionManagerBeanRef = new RuntimeBeanReference(transactionManagerRef); - bd.getPropertyValues().addPropertyValue("transactionManager", transactionManagerBeanRef); - - handleListenersElement(stepElement, bd, parserContext, "stepExecutionListeners"); - - bd.setRole(BeanDefinition.ROLE_SUPPORT); - - return bd; - - } - - /** - * @param element - * @param parserContext - * @return the TaskletStep bean - */ - protected RootBeanDefinition handleTaskletElement(Element stepElement, Element element, ParserContext parserContext) { - - RootBeanDefinition bd; - - boolean isFaultTolerant = false; - - String skipLimit = element.getAttribute("skip-limit"); - if (!isFaultTolerant) { - isFaultTolerant = checkIntValueForFaultToleranceNeeded(skipLimit); - } - String retryLimit = element.getAttribute("retry-limit"); - if (!isFaultTolerant) { - isFaultTolerant = checkIntValueForFaultToleranceNeeded(retryLimit); - } - String cacheCapacity = element.getAttribute("cache-capacity"); - if (!isFaultTolerant) { - isFaultTolerant = checkIntValueForFaultToleranceNeeded(cacheCapacity); - } - String isReaderTransactionalQueue = element.getAttribute("is-reader-transactional-queue"); - if (!isFaultTolerant && StringUtils.hasText(isReaderTransactionalQueue)) { - if ("true".equals(isReaderTransactionalQueue)) { - isFaultTolerant = true; - } - } - checkExceptionElementForFaultToleranceNeeded(element, "skippable-exception-classes"); - checkExceptionElementForFaultToleranceNeeded(element, "retryable-exception-classes"); - checkExceptionElementForFaultToleranceNeeded(element, "fatal-exception-classes"); - - if (isFaultTolerant) { - bd = new RootBeanDefinition("org.springframework.batch.core.step.item.FaultTolerantStepFactoryBean", null, null); - } - else { - bd = new RootBeanDefinition("org.springframework.batch.core.step.item.SimpleStepFactoryBean", null, null); - } - - // now, set the properties on the new bean - String startLimit = element.getAttribute("start-limit"); - if (StringUtils.hasText(startLimit)) { - bd.getPropertyValues().addPropertyValue("startLimit", startLimit); - } - String allowStartIfComplete = element.getAttribute("allow-start-if-complete"); - if (StringUtils.hasText(allowStartIfComplete)) { - bd.getPropertyValues().addPropertyValue("allowStartIfComplete", allowStartIfComplete); - } - - String readerBeanId = element.getAttribute("reader"); - if (StringUtils.hasText(readerBeanId)) { - RuntimeBeanReference readerRef = new RuntimeBeanReference(readerBeanId); - bd.getPropertyValues().addPropertyValue("itemReader", readerRef); - } - - String processorBeanId = element.getAttribute("processor"); - if (StringUtils.hasText(processorBeanId)) { - RuntimeBeanReference processorRef = new RuntimeBeanReference(processorBeanId); - bd.getPropertyValues().addPropertyValue("itemProcessor", processorRef); - } - - String writerBeanId = element.getAttribute("writer"); - if (StringUtils.hasText(writerBeanId)) { - RuntimeBeanReference writerRef = new RuntimeBeanReference(writerBeanId); - bd.getPropertyValues().addPropertyValue("itemWriter", writerRef); - } - - String taskExecutorBeanId = element.getAttribute("task-executor"); - if (StringUtils.hasText(taskExecutorBeanId)) { - RuntimeBeanReference taskExecutorRef = new RuntimeBeanReference(taskExecutorBeanId); - bd.getPropertyValues().addPropertyValue("taskExecutor", taskExecutorRef); - } - - String jobRepository = stepElement.getAttribute("job-repository"); - RuntimeBeanReference jobRepositoryRef = new RuntimeBeanReference(jobRepository); - bd.getPropertyValues().addPropertyValue("jobRepository", jobRepositoryRef); - - String transactionManager = stepElement.getAttribute("transaction-manager"); - RuntimeBeanReference tx = new RuntimeBeanReference(transactionManager); - bd.getPropertyValues().addPropertyValue("transactionManager", tx); - - String commitInterval = element.getAttribute("commit-interval"); - if (StringUtils.hasText(commitInterval)) { - bd.getPropertyValues().addPropertyValue("commitInterval", commitInterval); - } - - if (StringUtils.hasText(skipLimit)) { - bd.getPropertyValues().addPropertyValue("skipLimit", skipLimit); - } - - if (StringUtils.hasText(retryLimit)) { - bd.getPropertyValues().addPropertyValue("retryLimit", retryLimit); - } - - if (StringUtils.hasText(cacheCapacity)) { - bd.getPropertyValues().addPropertyValue("cacheCapacity", cacheCapacity); - } - - String transactionAttribute = element.getAttribute("transaction-attribute"); - if (StringUtils.hasText(transactionAttribute)) { - bd.getPropertyValues().addPropertyValue("transactionAttribute", transactionAttribute); - } - - if (StringUtils.hasText(isReaderTransactionalQueue)) { - if (isFaultTolerant) { - bd.getPropertyValues().addPropertyValue("isReaderTransactionalQueue", isReaderTransactionalQueue); - } - } - - handleExceptionElement(element, parserContext, bd, "skippable-exception-classes", "skippableExceptionClasses", isFaultTolerant); - - handleExceptionElement(element, parserContext, bd, "retryable-exception-classes", "retryableExceptionClasses", isFaultTolerant); - - handleExceptionElement(element, parserContext, bd, "fatal-exception-classes", "fatalExceptionClasses", isFaultTolerant); - - handleListenersElement(stepElement, bd, parserContext, "listeners"); - - handleRetryListenersElement(element, bd, parserContext); - - handleStreamsElement(element, bd, parserContext); - - bd.setRole(BeanDefinition.ROLE_SUPPORT); - - String id = parserContext.getReaderContext().generateBeanName(bd); - parserContext.getRegistry().registerBeanDefinition(id, bd); - - return bd; - - } - - private boolean checkIntValueForFaultToleranceNeeded(String stringValue) { - if (StringUtils.hasText(stringValue)) { - int value = Integer.valueOf(stringValue); - if (value > 0) { - return true; - } - } - return false; - } - - private boolean checkExceptionElementForFaultToleranceNeeded(Element element, String subElementName) { - String exceptions = - DomUtils.getChildElementValueByTagName(element, subElementName); - if (StringUtils.hasLength(exceptions)) { - return true; - } - return false; - } - - private void handleExceptionElement(Element element, ParserContext parserContext, RootBeanDefinition bd, - String subElementName, String propertyName, boolean isFaultTolerant) { - String exceptions = - DomUtils.getChildElementValueByTagName(element, subElementName); - if (StringUtils.hasLength(exceptions)) { - if (isFaultTolerant) { - String[] exceptionArray = StringUtils.tokenizeToStringArray( - StringUtils.delete(exceptions, ","), "\n"); - if (exceptionArray.length > 0) { - bd.getPropertyValues().addPropertyValue(propertyName, exceptionArray); - } - } - else { - parserContext.getReaderContext().error(subElementName + " can only be specified for fault-tolerant " + - "configurations providing skip-limit, retry-limit or cache-capacity", element); - } - } - } - - @SuppressWarnings("unchecked") - private void handleListenersElement(Element element, RootBeanDefinition bd, ParserContext parserContext, String property) { - Element listenersElement = - DomUtils.getChildElementByTagName(element, "listeners"); - if (listenersElement != null) { - List listenerBeans = new ArrayList(); - handleStepListenerElements(parserContext, listenersElement, - listenerBeans); - ManagedList arguments = new ManagedList(); - arguments.addAll(listenerBeans); - bd.getPropertyValues().addPropertyValue(property, arguments); - } - } - - @SuppressWarnings("unchecked") - private void handleRetryListenersElement(Element element, RootBeanDefinition bd, ParserContext parserContext) { - Element retryListenersElement = - DomUtils.getChildElementByTagName(element, "retry-listeners"); - if (retryListenersElement != null) { - List retryListenerBeans = new ArrayList(); - handleListenerElements(parserContext, retryListenersElement, - retryListenerBeans); - ManagedList arguments = new ManagedList(); - arguments.addAll(retryListenerBeans); - bd.getPropertyValues().addPropertyValue("retryListeners", arguments); - } - } - - @SuppressWarnings("unchecked") - private void handleListenerElements(ParserContext parserContext, - Element element, List beans) { - List listenerElements = - DomUtils.getChildElementsByTagName(element, "listener"); - if (listenerElements != null) { - for (Element listenerElement : listenerElements) { - String id = listenerElement.getAttribute("id"); - String listenerRef = listenerElement.getAttribute("ref"); - String className = listenerElement.getAttribute("class"); - checkListenerElementAttributes(parserContext, element, - listenerElement, id, listenerRef, className); - if (StringUtils.hasText(listenerRef)) { - BeanReference bean = new RuntimeBeanReference(listenerRef); - beans.add(bean); - } - else if (StringUtils.hasText(className)) { - RootBeanDefinition beanDef = new RootBeanDefinition(className, null, null); - if (!StringUtils.hasText(id)) { - id = parserContext.getReaderContext().generateBeanName(beanDef); - } - parserContext.getRegistry().registerBeanDefinition(id, beanDef); - BeanReference bean = new RuntimeBeanReference(id); - beans.add(bean); - } - else { - parserContext.getReaderContext().error("Neither 'ref' or 'class' specified for <" + listenerElement.getTagName() + "> element", element); - } - } - } - } - - @SuppressWarnings("unchecked") - private void handleStepListenerElements(ParserContext parserContext, - Element element, List beans) { - List listenerElements = - DomUtils.getChildElementsByTagName(element, "listener"); - if (listenerElements != null) { - for (Element listenerElement : listenerElements) { - BeanDefinitionBuilder listenerBuilder = - BeanDefinitionBuilder.genericBeanDefinition("org.springframework.batch.core.listener.StepListenerFactoryBean"); - String id = listenerElement.getAttribute("id"); - String listenerRef = listenerElement.getAttribute("ref"); - String className = listenerElement.getAttribute("class"); - checkListenerElementAttributes(parserContext, element, - listenerElement, id, listenerRef, className); - if (StringUtils.hasText(listenerRef)) { - listenerBuilder.addPropertyReference("delegate", listenerRef); - } - else if (StringUtils.hasText(className)) { - RootBeanDefinition beanDef = new RootBeanDefinition(className, null, null); - String delegateId = parserContext.getReaderContext().generateBeanName(beanDef); - parserContext.getRegistry().registerBeanDefinition(delegateId, beanDef); - listenerBuilder.addPropertyReference("delegate", delegateId); - } - else { - parserContext.getReaderContext().error("Neither 'ref' or 'class' specified for <" + listenerElement.getTagName() + "> element", element); - } - - ManagedMap metaDataMap = new ManagedMap(); - String[] methodNameAttributes = new String[] { - "before-step-method", - "after-step-method", - "before-chunk-method", - "after-chunk-method", - "before-read-method", - "after-read-method", - "on-read-error-method", - "before-process-method", - "after-process-method", - "on-process-error-method", - "before-write-method", - "after-write-method", - "on-write-error-method", - "on-skip-in-read-method", - "on-skip-in-process-method", - "on-skip-in-write-method" - }; - for (String metaDataPropertyName : methodNameAttributes) { - String listenerMethod = listenerElement.getAttribute(metaDataPropertyName); - if(StringUtils.hasText(listenerMethod)){ - metaDataMap.put(metaDataPropertyName, listenerMethod); - } - } - listenerBuilder.addPropertyValue("metaDataMap", metaDataMap); - - AbstractBeanDefinition beanDef = listenerBuilder.getBeanDefinition(); - if (!StringUtils.hasText(id)) { - id = parserContext.getReaderContext().generateBeanName(beanDef); - } - parserContext.getRegistry().registerBeanDefinition(id, beanDef); - BeanReference bean = new RuntimeBeanReference(id); - beans.add(bean); - } - } - } - - private void checkListenerElementAttributes(ParserContext parserContext, - Element element, Element listenerElement, String id, - String listenerRef, String className) { - if ((StringUtils.hasText(id) || StringUtils.hasText(className)) - && StringUtils.hasText(listenerRef)) { - NamedNodeMap attributeNodes = listenerElement.getAttributes(); - StringBuilder attributes = new StringBuilder(); - for (int i = 0; i < attributeNodes.getLength(); i++) { - if (i > 0) { - attributes.append(" "); - } - attributes.append(attributeNodes.item(i)); - } - parserContext.getReaderContext().error("Both 'ref' and " + - (StringUtils.hasText(id) ? "'id'" : "'class'") + - " specified; use 'class' with an optional 'id' or just 'ref' for <" + - listenerElement.getTagName() + "> element specified with attributes: " + attributes, element); - } - } - - @SuppressWarnings("unchecked") - private void handleStreamsElement(Element element, RootBeanDefinition bd, ParserContext parserContext) { - Element streamsElement = - DomUtils.getChildElementByTagName(element, "streams"); - if (streamsElement != null) { - List streamBeans = new ArrayList(); - List streamElements = - DomUtils.getChildElementsByTagName(streamsElement, "stream"); - if (streamElements != null) { - for (Element streamElement : streamElements) { - String streamRef = streamElement.getAttribute("ref"); - if (StringUtils.hasText(streamRef)) { - BeanReference bean = new RuntimeBeanReference(streamRef); - streamBeans.add(bean); - } - else { - parserContext.getReaderContext().error("ref not specified for <" + streamElement.getTagName() + "> element", element); - } - } - } - ManagedList arguments = new ManagedList(); - arguments.addAll(streamBeans); - bd.getPropertyValues().addPropertyValue("streams", arguments); - } - } - -} +/* + * Copyright 2006-2009 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.batch.core.configuration.xml; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanReference; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; + +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; + +/** + * Internal parser for the <step/> elements inside a job. A step element + * references a bean definition for a {@link org.springframework.batch.core.Step} and goes on to (optionally) + * list a set of transitions from that step to others with <next on="pattern" + * to="stepName"/>. Used by the {@link JobParser}. + * + * @see JobParser + * + * @author Dave Syer + * @author Thomas Risberg + * @since 2.0 + */ +public class StepParser { + + // For generating unique state names for end transitions + private static int endCounter = 0; + + /** + * Parse the step and turn it into a list of transitions. + * + * @param element the <step/gt; element to parse + * @param parserContext the parser context for the bean factory + * @return a collection of bean definitions for {@link org.springframework.batch.core.job.flow.support.StateTransition} + * instances objects + */ + public Collection parse(Element element, ParserContext parserContext) { + + BeanDefinitionBuilder stateBuilder = + BeanDefinitionBuilder.genericBeanDefinition("org.springframework.batch.core.job.flow.support.state.StepState"); + String stepRef = element.getAttribute("name"); + String taskletRef = element.getAttribute("tasklet"); + + @SuppressWarnings("unchecked") + List processTaskElements = (List) DomUtils.getChildElementsByTagName(element, "tasklet"); + if (StringUtils.hasText(taskletRef)) { + BeanDefinition task = handleTaskletRef(element, taskletRef, parserContext); + parserContext.getRegistry().registerBeanDefinition(stepRef, task); + stateBuilder.addConstructorArgReference(stepRef); + } + else if (processTaskElements.size() > 0) { + BeanDefinition task = handleTaskletElement(element, processTaskElements.get(0), parserContext); + parserContext.getRegistry().registerBeanDefinition(stepRef, task); + stateBuilder.addConstructorArgReference(stepRef); + } + else if (StringUtils.hasText(stepRef)) { + stateBuilder.addConstructorArgReference(stepRef); + } + else { + parserContext.getReaderContext().error("Incomplete configuration detected while creating step with name " + stepRef, element); + } + return getNextElements(parserContext, stateBuilder.getBeanDefinition(), element); + + } + + /** + * @param parserContext + * @param stateDef + * @param element + * @return a collection of {@link org.springframework.batch.core.job.flow.support.StateTransition} references + */ + public static Collection getNextElements(ParserContext parserContext, + BeanDefinition stateDef, Element element) { + + Collection list = new ArrayList(); + + String shortNextAttribute = element.getAttribute("next"); + boolean hasNextAttribute = StringUtils.hasText(shortNextAttribute); + if (hasNextAttribute) { + list.add(getStateTransitionReference(parserContext, stateDef, null, shortNextAttribute)); + } + + @SuppressWarnings("unchecked") + List nextElements = (List) DomUtils.getChildElementsByTagName(element, "next"); + @SuppressWarnings("unchecked") + List stopElements = (List) DomUtils.getChildElementsByTagName(element, "stop"); + nextElements.addAll(stopElements); + @SuppressWarnings("unchecked") + List endElements = (List) DomUtils.getChildElementsByTagName(element, "end"); + nextElements.addAll(endElements); + + for (Element nextElement : nextElements) { + String onAttribute = nextElement.getAttribute("on"); + String nextAttribute = nextElement.getAttribute("to"); + if (hasNextAttribute && onAttribute.equals("*")) { + parserContext.getReaderContext().error("Duplicate transition pattern found for '*' " + + "(only specify one of next= attribute at step level and next element with on='*')", + element); + } + + String name = nextElement.getNodeName(); + if ("stop".equals(name) || "end".equals(name)) { + + String statusName = nextElement.getAttribute("status"); + String status = StringUtils.hasText(statusName) ? statusName : "STOPPED"; + String nextOnEnd = StringUtils.hasText(statusName) ? null : nextAttribute; + + BeanDefinitionBuilder endBuilder = + BeanDefinitionBuilder.genericBeanDefinition("org.springframework.batch.core.job.flow.support.state.EndState"); + endBuilder.addConstructorArgValue(status); + String endName = "stop".equals(name) ? "end" + (endCounter++) : null; + + endBuilder.addConstructorArgValue(endName); + list.add(getStateTransitionReference(parserContext, endBuilder.getBeanDefinition(), onAttribute, nextOnEnd)); + nextAttribute = endName; + + } + list.add(getStateTransitionReference(parserContext, stateDef, onAttribute, nextAttribute)); + } + + if(hasNextAttribute && nextElements.isEmpty()) + { + list.add(getStateTransitionReference(parserContext, stateDef, "FAILED", null)); + } + + if (list.isEmpty() && !hasNextAttribute) { + list.add(getStateTransitionReference(parserContext, stateDef, null, null)); + } + + return list; + } + + /** + * @param parserContext the parser context + * @param stateDefinition a reference to the state implementation + * @param on the pattern value + * @param next the next step id + * @return a bean definition for a {@link org.springframework.batch.core.job.flow.support.StateTransition} + */ + public static RuntimeBeanReference getStateTransitionReference(ParserContext parserContext, + BeanDefinition stateDefinition, String on, String next) { + + BeanDefinitionBuilder nextBuilder = + BeanDefinitionBuilder.genericBeanDefinition("org.springframework.batch.core.job.flow.support.StateTransition"); + nextBuilder.addConstructorArgValue(stateDefinition); + + if (StringUtils.hasText(on)) { + nextBuilder.addConstructorArgValue(on); + } + + if (StringUtils.hasText(next)) { + nextBuilder.setFactoryMethod("createStateTransition"); + nextBuilder.addConstructorArgValue(next); + } + else { + nextBuilder.setFactoryMethod("createEndStateTransition"); + } + + // TODO: do we need to use RuntimeBeanReference? + AbstractBeanDefinition nextDef = nextBuilder.getBeanDefinition(); + String nextDefName = parserContext.getReaderContext().generateBeanName(nextDef); + BeanComponentDefinition nextDefComponent = new BeanComponentDefinition(nextDef, nextDefName); + parserContext.registerBeanComponent(nextDefComponent); + + return new RuntimeBeanReference(nextDefName); + + } + + /** + * @param stepElement + * @param taskletRef + * @param parserContext + * @return the TaskletStep bean + */ + protected BeanDefinition handleTaskletRef(Element stepElement, String taskletRef, ParserContext parserContext) { + + RootBeanDefinition bd = new RootBeanDefinition("org.springframework.batch.core.step.tasklet.TaskletStep", null, null); + + if (StringUtils.hasText(taskletRef)) { + RuntimeBeanReference taskletBeanRef = new RuntimeBeanReference(taskletRef); + bd.getPropertyValues().addPropertyValue("tasklet", taskletBeanRef); + } + + String jobRepositoryRef = stepElement.getAttribute("job-repository"); + RuntimeBeanReference jobRepositoryBeanRef = new RuntimeBeanReference(jobRepositoryRef); + bd.getPropertyValues().addPropertyValue("jobRepository", jobRepositoryBeanRef); + + String transactionManagerRef = stepElement.getAttribute("transaction-manager"); + RuntimeBeanReference transactionManagerBeanRef = new RuntimeBeanReference(transactionManagerRef); + bd.getPropertyValues().addPropertyValue("transactionManager", transactionManagerBeanRef); + + handleListenersElement(stepElement, bd, parserContext, "stepExecutionListeners"); + + bd.setRole(BeanDefinition.ROLE_SUPPORT); + + return bd; + + } + + /** + * @param element + * @param parserContext + * @return the TaskletStep bean + */ + protected BeanDefinition handleTaskletElement(Element stepElement, Element element, ParserContext parserContext) { + + RootBeanDefinition bd; + + boolean isFaultTolerant = false; + + String skipLimit = element.getAttribute("skip-limit"); + if (!isFaultTolerant) { + isFaultTolerant = checkIntValueForFaultToleranceNeeded(skipLimit); + } + String retryLimit = element.getAttribute("retry-limit"); + if (!isFaultTolerant) { + isFaultTolerant = checkIntValueForFaultToleranceNeeded(retryLimit); + } + String cacheCapacity = element.getAttribute("cache-capacity"); + if (!isFaultTolerant) { + isFaultTolerant = checkIntValueForFaultToleranceNeeded(cacheCapacity); + } + String isReaderTransactionalQueue = element.getAttribute("is-reader-transactional-queue"); + if (!isFaultTolerant && StringUtils.hasText(isReaderTransactionalQueue)) { + if ("true".equals(isReaderTransactionalQueue)) { + isFaultTolerant = true; + } + } + checkExceptionElementForFaultToleranceNeeded(element, "skippable-exception-classes"); + checkExceptionElementForFaultToleranceNeeded(element, "retryable-exception-classes"); + checkExceptionElementForFaultToleranceNeeded(element, "fatal-exception-classes"); + + if (isFaultTolerant) { + bd = new RootBeanDefinition("org.springframework.batch.core.step.item.FaultTolerantStepFactoryBean", null, null); + } + else { + bd = new RootBeanDefinition("org.springframework.batch.core.step.item.SimpleStepFactoryBean", null, null); + } + + // now, set the properties on the new bean + String startLimit = element.getAttribute("start-limit"); + if (StringUtils.hasText(startLimit)) { + bd.getPropertyValues().addPropertyValue("startLimit", startLimit); + } + String allowStartIfComplete = element.getAttribute("allow-start-if-complete"); + if (StringUtils.hasText(allowStartIfComplete)) { + bd.getPropertyValues().addPropertyValue("allowStartIfComplete", allowStartIfComplete); + } + + String readerBeanId = element.getAttribute("reader"); + if (StringUtils.hasText(readerBeanId)) { + RuntimeBeanReference readerRef = new RuntimeBeanReference(readerBeanId); + bd.getPropertyValues().addPropertyValue("itemReader", readerRef); + } + + String processorBeanId = element.getAttribute("processor"); + if (StringUtils.hasText(processorBeanId)) { + RuntimeBeanReference processorRef = new RuntimeBeanReference(processorBeanId); + bd.getPropertyValues().addPropertyValue("itemProcessor", processorRef); + } + + String writerBeanId = element.getAttribute("writer"); + if (StringUtils.hasText(writerBeanId)) { + RuntimeBeanReference writerRef = new RuntimeBeanReference(writerBeanId); + bd.getPropertyValues().addPropertyValue("itemWriter", writerRef); + } + + String taskExecutorBeanId = element.getAttribute("task-executor"); + if (StringUtils.hasText(taskExecutorBeanId)) { + RuntimeBeanReference taskExecutorRef = new RuntimeBeanReference(taskExecutorBeanId); + bd.getPropertyValues().addPropertyValue("taskExecutor", taskExecutorRef); + } + + String jobRepository = stepElement.getAttribute("job-repository"); + RuntimeBeanReference jobRepositoryRef = new RuntimeBeanReference(jobRepository); + bd.getPropertyValues().addPropertyValue("jobRepository", jobRepositoryRef); + + String transactionManager = stepElement.getAttribute("transaction-manager"); + RuntimeBeanReference tx = new RuntimeBeanReference(transactionManager); + bd.getPropertyValues().addPropertyValue("transactionManager", tx); + + String commitInterval = element.getAttribute("commit-interval"); + if (StringUtils.hasText(commitInterval)) { + bd.getPropertyValues().addPropertyValue("commitInterval", commitInterval); + } + + if (StringUtils.hasText(skipLimit)) { + bd.getPropertyValues().addPropertyValue("skipLimit", skipLimit); + } + + if (StringUtils.hasText(retryLimit)) { + bd.getPropertyValues().addPropertyValue("retryLimit", retryLimit); + } + + if (StringUtils.hasText(cacheCapacity)) { + bd.getPropertyValues().addPropertyValue("cacheCapacity", cacheCapacity); + } + + String transactionAttribute = element.getAttribute("transaction-attribute"); + if (StringUtils.hasText(transactionAttribute)) { + bd.getPropertyValues().addPropertyValue("transactionAttribute", transactionAttribute); + } + + if (StringUtils.hasText(isReaderTransactionalQueue)) { + if (isFaultTolerant) { + bd.getPropertyValues().addPropertyValue("isReaderTransactionalQueue", isReaderTransactionalQueue); + } + } + + handleExceptionElement(element, parserContext, bd, "skippable-exception-classes", "skippableExceptionClasses", isFaultTolerant); + + handleExceptionElement(element, parserContext, bd, "retryable-exception-classes", "retryableExceptionClasses", isFaultTolerant); + + handleExceptionElement(element, parserContext, bd, "fatal-exception-classes", "fatalExceptionClasses", isFaultTolerant); + + handleListenersElement(stepElement, bd, parserContext, "listeners"); + + handleRetryListenersElement(element, bd, parserContext); + + handleStreamsElement(element, bd, parserContext); + + bd.setRole(BeanDefinition.ROLE_SUPPORT); + + return bd; + + } + + private boolean checkIntValueForFaultToleranceNeeded(String stringValue) { + if (StringUtils.hasText(stringValue)) { + int value = Integer.valueOf(stringValue); + if (value > 0) { + return true; + } + } + return false; + } + + private boolean checkExceptionElementForFaultToleranceNeeded(Element element, String subElementName) { + String exceptions = + DomUtils.getChildElementValueByTagName(element, subElementName); + if (StringUtils.hasLength(exceptions)) { + return true; + } + return false; + } + + private void handleExceptionElement(Element element, ParserContext parserContext, BeanDefinition bd, + String subElementName, String propertyName, boolean isFaultTolerant) { + String exceptions = + DomUtils.getChildElementValueByTagName(element, subElementName); + if (StringUtils.hasLength(exceptions)) { + if (isFaultTolerant) { + String[] exceptionArray = StringUtils.tokenizeToStringArray( + StringUtils.delete(exceptions, ","), "\n"); + if (exceptionArray.length > 0) { + bd.getPropertyValues().addPropertyValue(propertyName, exceptionArray); + } + } + else { + parserContext.getReaderContext().error(subElementName + " can only be specified for fault-tolerant " + + "configurations providing skip-limit, retry-limit or cache-capacity", element); + } + } + } + + @SuppressWarnings("unchecked") + private void handleListenersElement(Element element, BeanDefinition bd, ParserContext parserContext, String property) { + Element listenersElement = + DomUtils.getChildElementByTagName(element, "listeners"); + if (listenersElement != null) { + List listenerBeans = new ArrayList(); + handleStepListenerElements(parserContext, listenersElement, + listenerBeans); + ManagedList arguments = new ManagedList(); + arguments.addAll(listenerBeans); + bd.getPropertyValues().addPropertyValue(property, arguments); + } + } + + @SuppressWarnings("unchecked") + private void handleRetryListenersElement(Element element, BeanDefinition bd, ParserContext parserContext) { + Element retryListenersElement = + DomUtils.getChildElementByTagName(element, "retry-listeners"); + if (retryListenersElement != null) { + List retryListenerBeans = new ArrayList(); + handleListenerElements(parserContext, retryListenersElement, + retryListenerBeans); + ManagedList arguments = new ManagedList(); + arguments.addAll(retryListenerBeans); + bd.getPropertyValues().addPropertyValue("retryListeners", arguments); + } + } + + @SuppressWarnings("unchecked") + private void handleListenerElements(ParserContext parserContext, + Element element, List beans) { + List listenerElements = + DomUtils.getChildElementsByTagName(element, "listener"); + if (listenerElements != null) { + for (Element listenerElement : listenerElements) { + String id = listenerElement.getAttribute("id"); + String listenerRef = listenerElement.getAttribute("ref"); + String className = listenerElement.getAttribute("class"); + checkListenerElementAttributes(parserContext, element, + listenerElement, id, listenerRef, className); + if (StringUtils.hasText(listenerRef)) { + BeanReference bean = new RuntimeBeanReference(listenerRef); + beans.add(bean); + } + else if (StringUtils.hasText(className)) { + RootBeanDefinition beanDef = new RootBeanDefinition(className, null, null); + if (!StringUtils.hasText(id)) { + id = parserContext.getReaderContext().generateBeanName(beanDef); + } + parserContext.getRegistry().registerBeanDefinition(id, beanDef); + BeanReference bean = new RuntimeBeanReference(id); + beans.add(bean); + } + else { + parserContext.getReaderContext().error("Neither 'ref' or 'class' specified for <" + listenerElement.getTagName() + "> element", element); + } + } + } + } + + @SuppressWarnings("unchecked") + private void handleStepListenerElements(ParserContext parserContext, + Element element, List beans) { + List listenerElements = + DomUtils.getChildElementsByTagName(element, "listener"); + if (listenerElements != null) { + for (Element listenerElement : listenerElements) { + BeanDefinitionBuilder listenerBuilder = + BeanDefinitionBuilder.genericBeanDefinition("org.springframework.batch.core.listener.StepListenerFactoryBean"); + String id = listenerElement.getAttribute("id"); + String listenerRef = listenerElement.getAttribute("ref"); + String className = listenerElement.getAttribute("class"); + checkListenerElementAttributes(parserContext, element, + listenerElement, id, listenerRef, className); + if (StringUtils.hasText(listenerRef)) { + listenerBuilder.addPropertyReference("delegate", listenerRef); + } + else if (StringUtils.hasText(className)) { + RootBeanDefinition beanDef = new RootBeanDefinition(className, null, null); + String delegateId = parserContext.getReaderContext().generateBeanName(beanDef); + parserContext.getRegistry().registerBeanDefinition(delegateId, beanDef); + listenerBuilder.addPropertyReference("delegate", delegateId); + } + else { + parserContext.getReaderContext().error("Neither 'ref' or 'class' specified for <" + listenerElement.getTagName() + "> element", element); + } + + ManagedMap metaDataMap = new ManagedMap(); + String[] methodNameAttributes = new String[] { + "before-step-method", + "after-step-method", + "before-chunk-method", + "after-chunk-method", + "before-read-method", + "after-read-method", + "on-read-error-method", + "before-process-method", + "after-process-method", + "on-process-error-method", + "before-write-method", + "after-write-method", + "on-write-error-method", + "on-skip-in-read-method", + "on-skip-in-process-method", + "on-skip-in-write-method" + }; + for (String metaDataPropertyName : methodNameAttributes) { + String listenerMethod = listenerElement.getAttribute(metaDataPropertyName); + if(StringUtils.hasText(listenerMethod)){ + metaDataMap.put(metaDataPropertyName, listenerMethod); + } + } + listenerBuilder.addPropertyValue("metaDataMap", metaDataMap); + + AbstractBeanDefinition beanDef = listenerBuilder.getBeanDefinition(); + if (!StringUtils.hasText(id)) { + id = parserContext.getReaderContext().generateBeanName(beanDef); + } + parserContext.getRegistry().registerBeanDefinition(id, beanDef); + BeanReference bean = new RuntimeBeanReference(id); + beans.add(bean); + } + } + } + + private void checkListenerElementAttributes(ParserContext parserContext, + Element element, Element listenerElement, String id, + String listenerRef, String className) { + if ((StringUtils.hasText(id) || StringUtils.hasText(className)) + && StringUtils.hasText(listenerRef)) { + NamedNodeMap attributeNodes = listenerElement.getAttributes(); + StringBuilder attributes = new StringBuilder(); + for (int i = 0; i < attributeNodes.getLength(); i++) { + if (i > 0) { + attributes.append(" "); + } + attributes.append(attributeNodes.item(i)); + } + parserContext.getReaderContext().error("Both 'ref' and " + + (StringUtils.hasText(id) ? "'id'" : "'class'") + + " specified; use 'class' with an optional 'id' or just 'ref' for <" + + listenerElement.getTagName() + "> element specified with attributes: " + attributes, element); + } + } + + @SuppressWarnings("unchecked") + private void handleStreamsElement(Element element, BeanDefinition bd, ParserContext parserContext) { + Element streamsElement = + DomUtils.getChildElementByTagName(element, "streams"); + if (streamsElement != null) { + List streamBeans = new ArrayList(); + List streamElements = + DomUtils.getChildElementsByTagName(streamsElement, "stream"); + if (streamElements != null) { + for (Element streamElement : streamElements) { + String streamRef = streamElement.getAttribute("ref"); + if (StringUtils.hasText(streamRef)) { + BeanReference bean = new RuntimeBeanReference(streamRef); + streamBeans.add(bean); + } + else { + parserContext.getReaderContext().error("ref not specified for <" + streamElement.getTagName() + "> element", element); + } + } + } + ManagedList arguments = new ManagedList(); + arguments.addAll(streamBeans); + bd.getPropertyValues().addPropertyValue("streams", arguments); + } + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StepParserTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StepParserTests.java index cec5fa8f0..dfdc68138 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StepParserTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StepParserTests.java @@ -15,13 +15,14 @@ */ package org.springframework.batch.core.configuration.xml; -import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.util.Map; import org.junit.Test; +import org.springframework.batch.core.Step; import org.springframework.batch.core.step.item.FaultTolerantStepFactoryBean; import org.springframework.batch.core.step.tasklet.TaskletStep; import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; @@ -45,6 +46,16 @@ public class StepParserTests { TaskletStep bean = (TaskletStep) factory.getObject(); assertEquals("wrong start-limit:", 25, bean.getStartLimit()); } + + @SuppressWarnings("unchecked") + @Test + public void testStepParserBeanName() throws Exception { + ConfigurableApplicationContext ctx = + new ClassPathXmlApplicationContext("org/springframework/batch/core/configuration/xml/StepParserBeanNameTests-context.xml"); + Map beans = ctx.getBeansOfType(Step.class); + assertTrue("'step1' bean not found", beans.containsKey("step1")); + assertTrue("'step2' bean not found", beans.containsKey("step2")); + } @Test public void testTaskletStepWithBadStepListener() throws Exception { diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StepParserBeanNameTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StepParserBeanNameTests-context.xml new file mode 100644 index 000000000..a5df2be5a --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StepParserBeanNameTests-context.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file