diff --git a/spring-batch-core/pom.xml b/spring-batch-core/pom.xml index cd9ed54c7..4c3269365 100644 --- a/spring-batch-core/pom.xml +++ b/spring-batch-core/pom.xml @@ -18,6 +18,11 @@ spring-batch-infrastructure ${project.version} + + javax.batch + javax.batch-api + 1.0-b29 + org.hsqldb hsqldb @@ -38,10 +43,6 @@ true test - - - - junit junit diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/EnableBatchProcessing.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/EnableBatchProcessing.java index aaf15b2dc..ad0331476 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/EnableBatchProcessing.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/EnableBatchProcessing.java @@ -21,6 +21,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import javax.sql.DataSource; + import org.springframework.batch.core.configuration.JobRegistry; import org.springframework.batch.core.configuration.support.ApplicationContextFactory; import org.springframework.batch.core.configuration.support.AutomaticJobRegistrar; diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/AbstractFlowParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/AbstractFlowParser.java index 9f91af2f1..0d6d3827c 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/AbstractFlowParser.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/AbstractFlowParser.java @@ -211,7 +211,7 @@ public abstract class AbstractFlowParser extends AbstractSingleBeanDefinitionPar * @param reachableElementMap * @param accumulator a collection of reachable element names */ - private void findAllReachableElements(String startElement, Map> reachableElementMap, + protected void findAllReachableElements(String startElement, Map> reachableElementMap, Set accumulator) { Set reachableIds = reachableElementMap.get(startElement); accumulator.add(startElement); @@ -233,7 +233,7 @@ public abstract class AbstractFlowParser extends AbstractSingleBeanDefinitionPar * {@link org.springframework.batch.core.job.flow.support.StateTransition} * references */ - protected static Collection getNextElements(ParserContext parserContext, BeanDefinition stateDef, + public static Collection getNextElements(ParserContext parserContext, BeanDefinition stateDef, Element element) { return getNextElements(parserContext, null, stateDef, element); } @@ -248,7 +248,7 @@ public abstract class AbstractFlowParser extends AbstractSingleBeanDefinitionPar * {@link org.springframework.batch.core.job.flow.support.StateTransition} * references */ - protected static Collection getNextElements(ParserContext parserContext, String stepId, + public static Collection getNextElements(ParserContext parserContext, String stepId, BeanDefinition stateDef, Element element) { Collection list = new ArrayList(); @@ -283,7 +283,7 @@ public abstract class AbstractFlowParser extends AbstractSingleBeanDefinitionPar else if (hasNextAttribute) { parserContext.getReaderContext().error( "The <" + element.getNodeName() + "/> may not contain a '" + NEXT_ATTR - + "' attribute and a transition element", element); + + "' attribute and a transition element", element); } return list; @@ -390,7 +390,7 @@ public abstract class AbstractFlowParser extends AbstractSingleBeanDefinitionPar * @return the BatchStatus corresponding to the transition name */ private static FlowExecutionStatus getBatchStatusFromEndTransitionName(String elementName) { - elementName = stripNamespace(elementName); + elementName = stripNamespace(elementName); if (STOP_ELE.equals(elementName)) { return FlowExecutionStatus.STOPPED; } @@ -405,17 +405,17 @@ public abstract class AbstractFlowParser extends AbstractSingleBeanDefinitionPar } } - /** - * Strip the namespace from the element name if it exists. - */ - private static String stripNamespace(String elementName){ - if(elementName.startsWith("batch:")){ - return elementName.substring(6); - } - else{ - return elementName; - } - } + /** + * Strip the namespace from the element name if it exists. + */ + private static String stripNamespace(String elementName){ + if(elementName.startsWith("batch:")){ + return elementName.substring(6); + } + else{ + return elementName; + } + } /** * @param parserContext the parser context diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/ChunkElementParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/ChunkElementParser.java index f9b2ef44b..fa05bba2a 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/ChunkElementParser.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/ChunkElementParser.java @@ -15,14 +15,14 @@ */ package org.springframework.batch.core.configuration.xml; +import java.util.List; + import org.springframework.batch.core.listener.StepListenerMetaData; -import org.springframework.batch.core.step.item.ForceRollbackForWriteSkipException; import org.springframework.batch.repeat.policy.SimpleCompletionPolicy; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.RuntimeBeanReference; -import org.springframework.beans.factory.config.TypedStringValue; import org.springframework.beans.factory.parsing.CompositeComponentDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; @@ -30,12 +30,11 @@ import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.beans.factory.support.ManagedList; import org.springframework.beans.factory.support.ManagedMap; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; import org.w3c.dom.Element; -import java.util.List; - /** * Internal parser for the <chunk/> element inside a step. * @@ -108,18 +107,18 @@ public class ChunkElementParser { if (propertyValues.contains("commitInterval")) { parserContext.getReaderContext().error( "The <" + element.getNodeName() + "/> element must contain either '" + COMMIT_INTERVAL_ATTR - + "' " + "or '" + CHUNK_COMPLETION_POLICY_ATTR + "', but not both.", element); + + "' " + "or '" + CHUNK_COMPLETION_POLICY_ATTR + "', but not both.", element); } else { parserContext.getReaderContext().error( "The <" + element.getNodeName() + "/> element must contain either '" + COMMIT_INTERVAL_ATTR - + "' " + "or '" + CHUNK_COMPLETION_POLICY_ATTR + "'.", element); + + "' " + "or '" + CHUNK_COMPLETION_POLICY_ATTR + "'.", element); } } String skipLimit = element.getAttribute("skip-limit"); - ManagedMap skippableExceptions = handleExceptionElement(element, parserContext, "skippable-exception-classes"); + ManagedMap skippableExceptions = new ExceptionElementParser().parse(element, parserContext, "skippable-exception-classes"); if (StringUtils.hasText(skipLimit)) { if (skippableExceptions == null) { skippableExceptions = new ManagedMap(); @@ -128,6 +127,12 @@ public class ChunkElementParser { propertyValues.addPropertyValue("skipLimit", skipLimit); } if (skippableExceptions != null) { + List exceptionClassElements = DomUtils.getChildElementsByTagName(element, "skippable-exception-classes"); + + if(!CollectionUtils.isEmpty(exceptionClassElements)) { + skippableExceptions.setMergeEnabled(exceptionClassElements.get(0).hasAttribute(MERGE_ATTR) + && Boolean.valueOf(exceptionClassElements.get(0).getAttribute(MERGE_ATTR))); + } // Even if there is no retryLimit, we can still accept exception // classes for an abstract parent bean definition propertyValues.addPropertyValue("skippableExceptionClasses", skippableExceptions); @@ -137,7 +142,7 @@ public class ChunkElementParser { underspecified); String retryLimit = element.getAttribute("retry-limit"); - ManagedMap retryableExceptions = handleExceptionElement(element, parserContext, "retryable-exception-classes"); + ManagedMap retryableExceptions = new ExceptionElementParser().parse(element, parserContext, "retryable-exception-classes"); if (StringUtils.hasText(retryLimit)) { if (retryableExceptions == null) { retryableExceptions = new ManagedMap(); @@ -146,6 +151,12 @@ public class ChunkElementParser { propertyValues.addPropertyValue("retryLimit", retryLimit); } if (retryableExceptions != null) { + List exceptionClassElements = DomUtils.getChildElementsByTagName(element, "retryable-exception-classes"); + + if(!CollectionUtils.isEmpty(exceptionClassElements)) { + retryableExceptions.setMergeEnabled(exceptionClassElements.get(0).hasAttribute(MERGE_ATTR) + && Boolean.valueOf(exceptionClassElements.get(0).getAttribute(MERGE_ATTR))); + } // Even if there is no retryLimit, we can still accept exception // classes for an abstract parent bean definition propertyValues.addPropertyValue("retryableExceptionClasses", retryableExceptions); @@ -189,7 +200,7 @@ public class ChunkElementParser { if (StringUtils.hasText(refName)) { parserContext.getReaderContext().error( "The <" + element.getNodeName() + "/> element may not have both a '" + handlerName - + "' attribute and a <" + handlerName + "/> element.", element); + + "' attribute and a <" + handlerName + "/> element.", element); } handleItemHandlerElement(enclosing, propertyName, adapterClassName, propertyValues, children.get(0), parserContext); } @@ -204,7 +215,7 @@ public class ChunkElementParser { else if (required && !underspecified) { parserContext.getReaderContext().error( "The <" + element.getNodeName() + "/> element has neither a '" + handlerName - + "' attribute nor a <" + handlerName + "/> element.", element); + + "' attribute nor a <" + handlerName + "/> element.", element); } } @@ -220,7 +231,7 @@ public class ChunkElementParser { if (beanElements.size() + refElements.size() != 1) { parserContext.getReaderContext().error( "The <" + element.getNodeName() + "/> must have exactly one of either a <" + BEAN_ELE - + "/> element or a <" + REF_ELE + "/> element.", element); + + "/> element or a <" + REF_ELE + "/> element.", element); } else if (beanElements.size() == 1) { Element beanElement = beanElements.get(0); @@ -313,35 +324,35 @@ public class ChunkElementParser { propertyValues.addPropertyValue("streams", streamBeans); } } - - @SuppressWarnings("unchecked") - private ManagedMap handleExceptionElement(Element element, ParserContext parserContext, String exceptionListName) { - List children = DomUtils.getChildElementsByTagName(element, exceptionListName); - if (children.size() == 1) { - ManagedMap map = new ManagedMap(); - Element exceptionClassesElement = children.get(0); - map.setMergeEnabled(exceptionClassesElement.hasAttribute(MERGE_ATTR) - && Boolean.valueOf(exceptionClassesElement.getAttribute(MERGE_ATTR))); - addExceptionClasses("include", true, exceptionClassesElement, map, parserContext); - addExceptionClasses("exclude", false, exceptionClassesElement, map, parserContext); - map.put(ForceRollbackForWriteSkipException.class, true); - return map; - } - else if (children.size() > 1) { - parserContext.getReaderContext().error( - "The <" + exceptionListName + "/> element may not appear more than once in a single <" - + element.getNodeName() + "/>.", element); - } - return null; - } - - @SuppressWarnings("unchecked") - private void addExceptionClasses(String elementName, boolean include, Element exceptionClassesElement, - ManagedMap map, ParserContext parserContext) { - for (Element child : (List) DomUtils.getChildElementsByTagName(exceptionClassesElement, elementName)) { - String className = child.getAttribute("class"); - map.put(new TypedStringValue(className, Class.class), include); - } - } - + // + // @SuppressWarnings("unchecked") + // private ManagedMap handleExceptionElement(Element element, ParserContext parserContext, String exceptionListName) { + // List children = DomUtils.getChildElementsByTagName(element, exceptionListName); + // if (children.size() == 1) { + // ManagedMap map = new ManagedMap(); + // Element exceptionClassesElement = children.get(0); + // map.setMergeEnabled(exceptionClassesElement.hasAttribute(MERGE_ATTR) + // && Boolean.valueOf(exceptionClassesElement.getAttribute(MERGE_ATTR))); + // addExceptionClasses("include", true, exceptionClassesElement, map, parserContext); + // addExceptionClasses("exclude", false, exceptionClassesElement, map, parserContext); + // map.put(ForceRollbackForWriteSkipException.class, true); + // return map; + // } + // else if (children.size() > 1) { + // parserContext.getReaderContext().error( + // "The <" + exceptionListName + "/> element may not appear more than once in a single <" + // + element.getNodeName() + "/>.", element); + // } + // return null; + // } + // + // @SuppressWarnings("unchecked") + // private void addExceptionClasses(String elementName, boolean include, Element exceptionClassesElement, + // ManagedMap map, ParserContext parserContext) { + // for (Element child : (List) DomUtils.getChildElementsByTagName(exceptionClassesElement, elementName)) { + // String className = child.getAttribute("class"); + // map.put(new TypedStringValue(className, Class.class), include); + // } + // } + // } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespaceUtils.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespaceUtils.java index 426b038ba..4963a5da1 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespaceUtils.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespaceUtils.java @@ -47,7 +47,7 @@ public class CoreNamespaceUtils { private static final String CORE_NAMESPACE_POST_PROCESSOR_CLASS_NAME = "org.springframework.batch.core.configuration.xml.CoreNamespacePostProcessor"; - protected static void autoregisterBeansForNamespace(ParserContext parserContext, Object source) { + public static void autoregisterBeansForNamespace(ParserContext parserContext, Object source) { checkForStepScope(parserContext, source); addRangePropertyEditor(parserContext); addCoreNamespacePostProcessor(parserContext); diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/ExceptionElementParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/ExceptionElementParser.java new file mode 100644 index 000000000..a5bf3501b --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/ExceptionElementParser.java @@ -0,0 +1,41 @@ +package org.springframework.batch.core.configuration.xml; + +import java.util.List; + +import org.springframework.batch.core.step.item.ForceRollbackForWriteSkipException; +import org.springframework.beans.factory.config.TypedStringValue; +import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.util.xml.DomUtils; +import org.w3c.dom.Element; + +public class ExceptionElementParser { + + @SuppressWarnings("unchecked") + public ManagedMap parse(Element element, ParserContext parserContext, String exceptionListName) { + List children = DomUtils.getChildElementsByTagName(element, exceptionListName); + if (children.size() == 1) { + ManagedMap map = new ManagedMap(); + Element exceptionClassesElement = children.get(0); + addExceptionClasses("include", true, exceptionClassesElement, map, parserContext); + addExceptionClasses("exclude", false, exceptionClassesElement, map, parserContext); + map.put(ForceRollbackForWriteSkipException.class, true); + return map; + } + else if (children.size() > 1) { + parserContext.getReaderContext().error( + "The <" + exceptionListName + "/> element may not appear more than once in a single <" + + element.getNodeName() + "/>.", element); + } + return null; + } + + @SuppressWarnings("unchecked") + private void addExceptionClasses(String elementName, boolean include, Element exceptionClassesElement, + ManagedMap map, ParserContext parserContext) { + for (Element child : DomUtils.getChildElementsByTagName(exceptionClassesElement, elementName)) { + String className = child.getAttribute("class"); + map.put(new TypedStringValue(className, Class.class), include); + } + } +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/JobParserJobFactoryBean.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/JobParserJobFactoryBean.java index 202281f63..45abbadc6 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/JobParserJobFactoryBean.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/JobParserJobFactoryBean.java @@ -35,7 +35,7 @@ import org.springframework.util.StringUtils; * @author Dave Syer * @since 2.0.1 */ -class JobParserJobFactoryBean implements SmartFactoryBean { +public class JobParserJobFactoryBean implements SmartFactoryBean { private String name; diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/SplitParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/SplitParser.java index 5209dc22b..cd3c12477 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/SplitParser.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/SplitParser.java @@ -84,14 +84,13 @@ public class SplitParser { stateBuilder.addPropertyValue("taskExecutor", taskExecutorRef); } - @SuppressWarnings("unchecked") List flowElements = DomUtils.getChildElementsByTagName(element, "flow"); if (flowElements.size() < 2) { parserContext.getReaderContext().error("A must contain at least two 'flow' elements.", element); } - @SuppressWarnings("unchecked") + @SuppressWarnings({"rawtypes", "unchecked"}) Collection flows = new ManagedList(); int i = 0; String prefix = idAttribute; diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/StepParserStepFactoryBean.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/StepParserStepFactoryBean.java index 9cecd4c43..b3a28f128 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/StepParserStepFactoryBean.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/StepParserStepFactoryBean.java @@ -59,7 +59,9 @@ import org.springframework.batch.item.ItemReader; import org.springframework.batch.item.ItemStream; import org.springframework.batch.item.ItemWriter; import org.springframework.batch.repeat.CompletionPolicy; +import org.springframework.batch.repeat.policy.CompositeCompletionPolicy; import org.springframework.batch.repeat.policy.SimpleCompletionPolicy; +import org.springframework.batch.repeat.policy.TimeoutTerminationPolicy; import org.springframework.batch.repeat.support.TaskExecutorRepeatTemplate; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.FactoryBean; @@ -89,7 +91,7 @@ import org.springframework.util.Assert; * @since 2.0 */ @SuppressWarnings("rawtypes") -class StepParserStepFactoryBean implements FactoryBean, BeanNameAware { +public class StepParserStepFactoryBean implements FactoryBean, BeanNameAware { // // Step Attributes @@ -188,6 +190,8 @@ class StepParserStepFactoryBean implements FactoryBean, BeanNameAware { private ItemWriter itemWriter; + private Integer timeout; + // // Chunk Elements // @@ -228,6 +232,7 @@ class StepParserStepFactoryBean implements FactoryBean, BeanNameAware { + "] has both a element and a 'ref' attribute referencing a Tasklet."); validateFaultTolerantSettings(); + if (isFaultTolerant()) { return createFaultTolerantStep(); } @@ -384,15 +389,26 @@ class StepParserStepFactoryBean implements FactoryBean, BeanNameAware { @SuppressWarnings("unchecked") private Step createSimpleStep() { SimpleStepBuilder builder = new SimpleStepBuilder(new StepBuilder(name)); - if (commitInterval != null) { + + if(timeout != null && commitInterval != null) { + CompositeCompletionPolicy completionPolicy = new CompositeCompletionPolicy(); + CompletionPolicy [] policies = new CompletionPolicy[2]; + policies[0] = new SimpleCompletionPolicy(commitInterval); + policies[1] = new TimeoutTerminationPolicy(timeout); + completionPolicy.setPolicies(policies); + builder.chunk(completionPolicy); + } else if(timeout != null) { + builder.chunk(new TimeoutTerminationPolicy(timeout * 1000)); + } else if(commitInterval != null) { builder.chunk(commitInterval); } + + builder.chunk(chunkCompletionPolicy); enhanceTaskletStepBuilder(builder); registerItemListeners(builder); builder.reader(itemReader); builder.writer(itemWriter); builder.processor(itemProcessor); - builder.chunk(chunkCompletionPolicy); return builder.build(); } @@ -975,6 +991,10 @@ class StepParserStepFactoryBean implements FactoryBean, BeanNameAware { this.streams = streams; } + public void setTimeout(Integer timeout) { + this.timeout = timeout; + } + // ========================================================= // Additional // ========================================================= diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/BatchletParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/BatchletParser.java new file mode 100644 index 000000000..a05549c3c --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/BatchletParser.java @@ -0,0 +1,55 @@ +/* + * Copyright 2013 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.jsr.configuration.xml; + +import org.springframework.batch.core.configuration.xml.StepParserStepFactoryBean; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.util.StringUtils; +import org.w3c.dom.Element; + +/** + * Parser for the <batchlet /> tag defined in JSR-352. The current state + * of this parser parses a batchlet element into a {@link Tasklet} (the ref + * attribute is expected to point to an implementation of Tasklet). + * + * @author Michael Minella + * @since 3.0 + */ +public class BatchletParser extends AbstractSingleBeanDefinitionParser { + + private static final String REF = "ref"; + + public void parseBatchlet(Element stepElement, Element taskletElement, AbstractBeanDefinition bd, + ParserContext parserContext) { + + bd.setBeanClass(StepParserStepFactoryBean.class); + bd.setAttribute("isNamespaceStep", true); + + String taskletRef = taskletElement.getAttribute(REF); + + if (StringUtils.hasText(taskletRef)) { + bd.getPropertyValues().addPropertyValue("tasklet", new RuntimeBeanReference(taskletRef)); + } + + bd.setRole(BeanDefinition.ROLE_SUPPORT); + bd.setSource(parserContext.extractSource(taskletElement)); + } +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/ChunkParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/ChunkParser.java new file mode 100644 index 000000000..0c7dfbf5a --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/ChunkParser.java @@ -0,0 +1,166 @@ +/* + * Copyright 2013 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.jsr.configuration.xml; + +import java.util.List; + +import org.springframework.batch.core.configuration.xml.ExceptionElementParser; +import org.springframework.batch.core.configuration.xml.StepParserStepFactoryBean; +import org.springframework.batch.core.step.item.ChunkOrientedTasklet; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemReader; +import org.springframework.batch.item.ItemWriter; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.config.TypedStringValue; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.support.ManagedMap; +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.Node; +import org.w3c.dom.NodeList; + +/** + * Parser for the <chunk /> element as specified in JSR-352. The current state + * parses a chunk element into it's related batch artifacts ({@link ChunkOrientedTasklet}, {@link ItemReader}, + * {@link ItemProcessor}, and {@link ItemWriter}). + * + * @author Michael Minella + * @since 3.0 + * + */ +public class ChunkParser { + + private static final String TIME_LIMIT_ATTRIBUTE = "time-limit"; + private static final String ITEM_COUNT_ATTRIBUTE = "item-count"; + private static final String CHECKPOINT_ALGORITHM_ELEMENT = "checkpoint-algorithm"; + private static final String CLASS_ATTRIBUTE = "class"; + private static final String INCLUDE_ELEMENT = "include"; + private static final String NO_ROLLBACK_EXCEPTION_CLASSES_ELEMENT = "no-rollback-exception-classes"; + private static final String RETRYABLE_EXCEPTION_CLASSES_ELEMENT = "retryable-exception-classes"; + private static final String SKIPPABLE_EXCEPTION_CLASSES_ELEMENT = "skippable-exception-classes"; + private static final String WRITER_ELEMENT = "writer"; + private static final String PROCESSOR_ELEMENT = "processor"; + private static final String READER_ELEMENT = "reader"; + private static final String REF_ATTRIBUTE = "ref"; + private static final String RETRY_LIMIT_ATTRIBUTE = "retry-limit"; + private static final String SKIP_LIMIT_ATTRIBUTE = "skip-limit"; + private static final String CUSTOM_CHECKPOINT_POLICY = "custom"; + private static final String ITEM_CHECKPOINT_POLICY = "item"; + private static final String CHECKPOINT_POLICY_ATTRIBUTE = "checkpoint-policy"; + + public void parse(Element element, AbstractBeanDefinition bd, ParserContext parserContext) { + MutablePropertyValues propertyValues = bd.getPropertyValues(); + bd.setBeanClass(StepParserStepFactoryBean.class); + + propertyValues.addPropertyValue("hasChunkElement", Boolean.TRUE); + + String checkpointPolicy = element.getAttribute(CHECKPOINT_POLICY_ATTRIBUTE); + if(StringUtils.hasText(checkpointPolicy)) { + if(checkpointPolicy.equals(ITEM_CHECKPOINT_POLICY)) { + parseSimpleAttribute(element, propertyValues, ITEM_COUNT_ATTRIBUTE, "commitInterval"); + parseSimpleAttribute(element, propertyValues, TIME_LIMIT_ATTRIBUTE, "timeout"); + } else if(checkpointPolicy.equals(CUSTOM_CHECKPOINT_POLICY)) { + parseCustomCheckpointAlgorithm(element, parserContext, + propertyValues); + } + } + + parseSimpleAttribute(element, propertyValues, SKIP_LIMIT_ATTRIBUTE, "skipLimit"); + parseSimpleAttribute(element, propertyValues, RETRY_LIMIT_ATTRIBUTE, "retryLimit"); + + NodeList children = element.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node nd = children.item(i); + + parseChildElement(element, parserContext, propertyValues, nd); + } + } + + private void parseSimpleAttribute(Element element, + MutablePropertyValues propertyValues, String attributeName, String propertyName) { + String skipLimit = element.getAttribute(attributeName); + if (StringUtils.hasText(skipLimit)) { + propertyValues.addPropertyValue(propertyName, skipLimit); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void parseChildElement(Element element, + ParserContext parserContext, MutablePropertyValues propertyValues, + Node nd) { + if (nd instanceof Element) { + Element nestedElement = (Element) nd; + String name = nestedElement.getLocalName(); + + String artifactName = nestedElement.getAttribute(REF_ATTRIBUTE); + if(name.equals(READER_ELEMENT)) { + if (StringUtils.hasText(artifactName)) { + propertyValues.addPropertyValue("itemReader", new RuntimeBeanReference(artifactName)); + } + } else if(name.equals(PROCESSOR_ELEMENT)) { + if (StringUtils.hasText(artifactName)) { + propertyValues.addPropertyValue("itemProcessor", new RuntimeBeanReference(artifactName)); + } + } else if(name.equals(WRITER_ELEMENT)) { + if (StringUtils.hasText(artifactName)) { + propertyValues.addPropertyValue("itemWriter", new RuntimeBeanReference(artifactName)); + } + } else if(name.equals(SKIPPABLE_EXCEPTION_CLASSES_ELEMENT)) { + ManagedMap exceptionClasses = new ExceptionElementParser().parse(element, parserContext, SKIPPABLE_EXCEPTION_CLASSES_ELEMENT); + if(exceptionClasses != null) { + propertyValues.addPropertyValue("skippableExceptionClasses", exceptionClasses); + } + } else if(name.equals(RETRYABLE_EXCEPTION_CLASSES_ELEMENT)) { + ManagedMap exceptionClasses = new ExceptionElementParser().parse(element, parserContext, RETRYABLE_EXCEPTION_CLASSES_ELEMENT); + if(exceptionClasses != null) { + propertyValues.addPropertyValue("retryableExceptionClasses", exceptionClasses); + } + } else if(name.equals(NO_ROLLBACK_EXCEPTION_CLASSES_ELEMENT)) { + //TODO: Update to support excludes + ManagedList list = new ManagedList(); + + for (Element child : DomUtils.getChildElementsByTagName(nestedElement, INCLUDE_ELEMENT)) { + String className = child.getAttribute(CLASS_ATTRIBUTE); + list.add(new TypedStringValue(className, Class.class)); + } + + propertyValues.addPropertyValue("noRollbackExceptionClasses", list); + } + } + } + + private void parseCustomCheckpointAlgorithm(Element element, + ParserContext parserContext, MutablePropertyValues propertyValues) { + List elements = DomUtils.getChildElementsByTagName(element, CHECKPOINT_ALGORITHM_ELEMENT); + + if(elements.size() == 1) { + Element checkpointAlgorithmElement = elements.get(0); + + String name = checkpointAlgorithmElement.getAttribute(REF_ATTRIBUTE); + if(StringUtils.hasText(name)) { + propertyValues.addPropertyValue("chunkCompletionPolicy", new RuntimeBeanReference(name)); + } + } else if(elements.size() > 1){ + parserContext.getReaderContext().error( + "The element may not appear more than once in a single <" + + element.getNodeName() + "/>.", element); + } + } +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/DecisionParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/DecisionParser.java new file mode 100644 index 000000000..c54597bf1 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/DecisionParser.java @@ -0,0 +1,50 @@ +/* + * Copyright 2013 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.jsr.configuration.xml; + +import java.util.Collection; + +import org.springframework.batch.core.job.flow.JobExecutionDecider; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.ParserContext; +import org.w3c.dom.Element; + +/** + * Parser for the <decision /> element as specified in JSR-352. The current state + * parses a decision element and assumes that it refers to a {@link JobExecutionDecider} + * + * @author Michael Minella + * @since 3.0 + */ +public class DecisionParser { + + private static final String ID_ATTRIBUTE = "id"; + private static final String REF_ATTRIBUTE = "ref"; + + public Collection parse(Element element, ParserContext parserContext) { + + String refAttribute = element.getAttribute(REF_ATTRIBUTE); + String idAttribute = element.getAttribute(ID_ATTRIBUTE); + + BeanDefinitionBuilder stateBuilder = + BeanDefinitionBuilder.genericBeanDefinition("org.springframework.batch.core.job.flow.support.state.DecisionState"); + stateBuilder.addConstructorArgValue(new RuntimeBeanReference(refAttribute)); + stateBuilder.addConstructorArgValue(idAttribute); + return FlowParser.getNextElements(parserContext, stateBuilder.getBeanDefinition(), element); + } +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/FlowParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/FlowParser.java new file mode 100644 index 000000000..b94e64571 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/FlowParser.java @@ -0,0 +1,97 @@ +/* + * Copyright 2013 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.jsr.configuration.xml; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.batch.core.configuration.xml.AbstractFlowParser; +import org.springframework.batch.core.configuration.xml.SimpleFlowFactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.xml.ParserContext; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * Parses flows as defined in JSR-352. The current state parses a flow + * as it is within a regular Spring Batch job/flow. + * + * @author Michael Minella + * @since 3.0 + */ +public class FlowParser extends AbstractFlowParser { + + private static final String DECISION_ELEMENT = "decision"; + private static final String SPLIT_ELEMENT = "split"; + private static final String STEP_ELEMENT = "step"; + private StepParser stepParser = new StepParser(); + private String flowName; + + public FlowParser(String flowName) { + this.flowName = flowName; + } + + @Override + protected Class getBeanClass(Element element) { + return SimpleFlowFactoryBean.class; + } + + @Override + @SuppressWarnings({"rawtypes", "unchecked"}) + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + builder.getRawBeanDefinition().setAttribute("flowName", flowName); + builder.addPropertyValue("name", flowName); + + List stateTransitions = new ArrayList(); + + Map> reachableElementMap = new HashMap>(); + String startElement = null; + NodeList children = element.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node node = children.item(i); + if (node instanceof Element) { + String nodeName = node.getLocalName(); + Element child = (Element) node; + if (nodeName.equals(STEP_ELEMENT)) { + stateTransitions.addAll(stepParser.parse(child, parserContext, builder)); + } else if(nodeName.equals(SPLIT_ELEMENT)) { + stateTransitions.addAll(new SplitParser().parse(child, parserContext)); + } else if(nodeName.equals(DECISION_ELEMENT)) { + stateTransitions.addAll(new DecisionParser().parse(child, parserContext)); + } + } + } + + Set allReachableElements = new HashSet(); + findAllReachableElements(startElement, reachableElementMap, allReachableElements); + for (String elementId : reachableElementMap.keySet()) { + if (!allReachableElements.contains(elementId)) { + parserContext.getReaderContext().error("The element [" + elementId + "] is unreachable", element); + } + } + + ManagedList managedList = new ManagedList(); + managedList.addAll(stateTransitions); + builder.addPropertyValue("stateTransitions", managedList); + } +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/JobParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/JobParser.java new file mode 100644 index 000000000..7d0e87822 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/JobParser.java @@ -0,0 +1,62 @@ +/* + * Copyright 2013 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.jsr.configuration.xml; + +import org.springframework.batch.core.configuration.xml.CoreNamespaceUtils; +import org.springframework.batch.core.configuration.xml.JobParserJobFactoryBean; +import org.springframework.batch.core.listener.JobListenerFactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.util.StringUtils; +import org.w3c.dom.Element; + +/** + * Parses a <job /> tag as defined in JSR-352. Current state parses into + * the standard Spring Batch artifacts. + * + * @author Michael Minella + * @since 3.0 + */ +public class JobParser extends AbstractSingleBeanDefinitionParser { + + private static final String RESTARTABLE_ATTRIBUTE = "restartable"; + private static final String ID_ATTRIBUTE = "id"; + + @Override + protected Class getBeanClass(Element element) { + return JobParserJobFactoryBean.class; + } + + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + CoreNamespaceUtils.autoregisterBeansForNamespace(parserContext, parserContext.extractSource(element)); + + String jobName = element.getAttribute(ID_ATTRIBUTE); + builder.addConstructorArgValue(jobName); + + String restartableAttribute = element.getAttribute(RESTARTABLE_ATTRIBUTE); + if (StringUtils.hasText(restartableAttribute)) { + builder.addPropertyValue("restartable", restartableAttribute); + } + + BeanDefinition flowDef = new FlowParser(jobName).parse(element, parserContext); + builder.addPropertyValue("flow", flowDef); + + new ListnerParser(JobListenerFactoryBean.class, "jobExecutionListeners").parseListeners(element, parserContext, builder); + } +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/JsrNamespaceHandler.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/JsrNamespaceHandler.java new file mode 100644 index 000000000..999281e75 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/JsrNamespaceHandler.java @@ -0,0 +1,31 @@ +/* + * Copyright 2013 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.jsr.configuration.xml; + +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; + +/** + * + * @author Michael Minella + * @since 3.0 + */ +public class JsrNamespaceHandler extends NamespaceHandlerSupport { + + @Override + public void init() { + this.registerBeanDefinitionParser("job", new JobParser()); + } +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/ListnerParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/ListnerParser.java new file mode 100644 index 000000000..a3fed34fd --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/ListnerParser.java @@ -0,0 +1,95 @@ +/* + * Copyright 2013 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.jsr.configuration.xml; + +import java.util.List; + +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +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.xml.ParserContext; +import org.springframework.util.xml.DomUtils; +import org.w3c.dom.Element; + +/** + * Parses the various listeners defined in JSR-352. Current state assumes + * the ref attributes point to implementations of Spring Batch interfaces + * and not JSR interfaces + * + * @author Michael Minella + * @since 3.0 + */ +public class ListnerParser { + + private static final String REF_ATTRIBUTE = "ref"; + private static final String LISTENER_ELEMENT = "listener"; + private static final String LISTENERS_ELEMENT = "listeners"; + @SuppressWarnings("rawtypes") + private Class listenerType; + private String propertyKey; + + @SuppressWarnings("rawtypes") + public ListnerParser(Class listenerType, String propertyKey) { + this.listenerType = listenerType; + this.propertyKey = propertyKey; + } + + public void parseListeners(Element element, ParserContext parserContext, AbstractBeanDefinition bd) { + ManagedList listeners = parseListeners(element, parserContext); + + if(listeners.size() > 0) { + bd.getPropertyValues().add(propertyKey, listeners); + } + } + + public void parseListeners(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + ManagedList listeners = parseListeners(element, parserContext); + + if(listeners.size() > 0) { + builder.addPropertyValue(propertyKey, listeners); + } + } + + private ManagedList parseListeners(Element element, ParserContext parserContext) { + + List listenersElements = DomUtils.getChildElementsByTagName(element, LISTENERS_ELEMENT); + + ManagedList listeners = new ManagedList(); + + if (listenersElements.size() == 1) { + Element listenersElement = listenersElements.get(0); + CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(listenersElement.getTagName(), + parserContext.extractSource(element)); + parserContext.pushContainingComponent(compositeDef); + listeners.setMergeEnabled(false); + List listenerElements = DomUtils.getChildElementsByTagName(listenersElement, LISTENER_ELEMENT); + for (Element listenerElement : listenerElements) { + BeanDefinitionBuilder bd = BeanDefinitionBuilder.genericBeanDefinition(listenerType); + bd.addPropertyValue("delegate", new RuntimeBeanReference(listenerElement.getAttribute(REF_ATTRIBUTE))); + listeners.add(bd.getBeanDefinition()); + } + parserContext.popAndRegisterContainingComponent(); + } + else if (listenersElements.size() > 1) { + parserContext.getReaderContext().error( + "The '' element may not appear more than once in a single .", element); + } + + return listeners; + } +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/SplitParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/SplitParser.java new file mode 100644 index 000000000..fdeba899d --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/SplitParser.java @@ -0,0 +1,63 @@ +/* + * Copyright 2013 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.jsr.configuration.xml; + +import java.util.Collection; +import java.util.List; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.util.xml.DomUtils; +import org.w3c.dom.Element; + +/** + * Parses a <split /> element as defined in JSR-352. + * + * @author Michael Minella + * @since 3.0 + */ +public class SplitParser { + + public Collection parse(Element element, ParserContext parserContext) { + + String idAttribute = element.getAttribute("id"); + + BeanDefinitionBuilder stateBuilder = BeanDefinitionBuilder + .genericBeanDefinition("org.springframework.batch.core.job.flow.support.state.SplitState"); + + List flowElements = DomUtils.getChildElementsByTagName(element, "flow"); + + if (flowElements.size() < 2) { + parserContext.getReaderContext().error("A must contain at least two 'flow' elements.", element); + } + + Collection flows = new ManagedList(); + int i = 0; + String prefix = idAttribute; + for (Element nextElement : flowElements) { + FlowParser flowParser = new FlowParser(prefix + "." + i); + flows.add(flowParser.parse(nextElement, parserContext)); + i++; + } + + stateBuilder.addConstructorArgValue(flows); + stateBuilder.addConstructorArgValue(prefix); + + return FlowParser.getNextElements(parserContext, null, stateBuilder.getBeanDefinition(), element); + } +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/StepParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/StepParser.java new file mode 100644 index 000000000..2214f316b --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/StepParser.java @@ -0,0 +1,98 @@ +/* + * Copyright 2013 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.jsr.configuration.xml; + +import java.util.Collection; + +import org.springframework.batch.core.configuration.xml.StepParserStepFactoryBean; +import org.springframework.batch.core.job.flow.support.state.StepState; +import org.springframework.batch.core.listener.StepListenerFactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; +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.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.util.StringUtils; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * Parser for the <step /> element defined by JSR-352. Current state parses it + * into existing Spring Batch artifacts. + * + * @author Michael Minella + * @since 3.0 + */ +public class StepParser extends AbstractSingleBeanDefinitionParser { + + private static final String CHUNK_ELEMENT = "chunk"; + private static final String BATCHLET_ELEMENT = "batchlet"; + private static final String ALLOW_START_IF_COMPLETE_ATTRIBUTE = "allow-start-if-complete"; + private static final String START_LIMIT_ATTRIBUTE = "start-limit"; + private static final String SPLIT_ID_ATTRIBUTE = "id"; + + @Override + @SuppressWarnings("rawtypes") + protected Class getBeanClass(Element element) { + return StepParserStepFactoryBean.class; + } + + protected Collection parse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + BeanDefinitionBuilder defBuilder = BeanDefinitionBuilder.genericBeanDefinition(); + AbstractBeanDefinition bd = defBuilder.getRawBeanDefinition(); + + BeanDefinitionBuilder stateBuilder = BeanDefinitionBuilder.genericBeanDefinition(StepState.class); + + String stepName = element.getAttribute(SPLIT_ID_ATTRIBUTE); + builder.addPropertyValue("name", stepName); + parserContext.registerBeanComponent(new BeanComponentDefinition(bd, stepName)); + stateBuilder.addConstructorArgReference(stepName); + + String startLimit = element.getAttribute(START_LIMIT_ATTRIBUTE); + if(StringUtils.hasText(startLimit)) { + builder.addPropertyValue("startLimit", startLimit); + } + + String allowStartIfComplete = element.getAttribute(ALLOW_START_IF_COMPLETE_ATTRIBUTE); + if(StringUtils.hasText(allowStartIfComplete)) { + builder.addPropertyValue("allowStartIfComplete", allowStartIfComplete); + } + + new ListnerParser(StepListenerFactoryBean.class, "listeners").parseListeners(element, parserContext, bd); + + // look at all nested elements + NodeList children = element.getChildNodes(); + + for (int i = 0; i < children.getLength(); i++) { + Node nd = children.item(i); + + if (nd instanceof Element) { + Element nestedElement = (Element) nd; + String name = nestedElement.getLocalName(); + + if(name.equalsIgnoreCase(BATCHLET_ELEMENT)) { + new BatchletParser().parseBatchlet(element, nestedElement, bd, parserContext); + } else if(name.equals(CHUNK_ELEMENT)) { + new ChunkParser().parse(nestedElement, bd, parserContext); + } + } + } + + return FlowParser.getNextElements(parserContext, stepName, stateBuilder.getBeanDefinition(), element); + } +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java index 05450eccf..a7f01c60b 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java @@ -429,10 +429,8 @@ public class FaultTolerantStepBuilder extends SimpleStepBuilder { * Register explicitly set item listeners and auto-register reader, processor and writer if applicable */ private void registerSkipListeners() { - // auto-register reader, processor and writer for (Object itemHandler : new Object[] { getReader(), getWriter(), getProcessor() }) { - if (StepListenerFactoryBean.isListener(itemHandler)) { StepListener listener = StepListenerFactoryBean.getListener(itemHandler); if (listener instanceof SkipListener) { diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/FaultTolerantChunkProvider.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/FaultTolerantChunkProvider.java index e8fcf0e8a..e22fbb5d2 100755 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/FaultTolerantChunkProvider.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/FaultTolerantChunkProvider.java @@ -16,8 +16,6 @@ package org.springframework.batch.core.step.item; -import org.springframework.classify.BinaryExceptionClassifier; -import org.springframework.classify.Classifier; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.step.skip.LimitCheckingItemSkipPolicy; import org.springframework.batch.core.step.skip.NonSkippableReadException; @@ -27,6 +25,8 @@ import org.springframework.batch.core.step.skip.SkipPolicy; import org.springframework.batch.core.step.skip.SkipPolicyFailedException; import org.springframework.batch.item.ItemReader; import org.springframework.batch.repeat.RepeatOperations; +import org.springframework.classify.BinaryExceptionClassifier; +import org.springframework.classify.Classifier; /** * FaultTolerant implementation of the {@link ChunkProcessor} interface, that @@ -53,7 +53,7 @@ public class FaultTolerantChunkProvider extends SimpleChunkProvider { public FaultTolerantChunkProvider(ItemReader itemReader, RepeatOperations repeatOperations) { super(itemReader, repeatOperations); } - + /** * @param maxSkipsOnRead the maximum number of skips on read */ @@ -89,6 +89,7 @@ public class FaultTolerantChunkProvider extends SimpleChunkProvider { catch (Exception e) { if (shouldSkip(skipPolicy, e, contribution.getStepSkipCount())) { + // increment skip count and try again contribution.incrementReadSkipCount(); chunk.skip(e); diff --git a/spring-batch-core/src/main/resources/META-INF/spring.handlers b/spring-batch-core/src/main/resources/META-INF/spring.handlers index fbdf08e4f..2663f5208 100644 --- a/spring-batch-core/src/main/resources/META-INF/spring.handlers +++ b/spring-batch-core/src/main/resources/META-INF/spring.handlers @@ -1 +1,2 @@ -http\://www.springframework.org/schema/batch=org.springframework.batch.core.configuration.xml.CoreNamespaceHandler \ No newline at end of file +http\://www.springframework.org/schema/batch=org.springframework.batch.core.configuration.xml.CoreNamespaceHandler +http\://xmlns.jcp.org/xml/ns/javaee=org.springframework.batch.core.jsr.configuration.xml.JsrNamespaceHandler \ No newline at end of file diff --git a/spring-batch-core/src/main/resources/META-INF/spring.schemas b/spring-batch-core/src/main/resources/META-INF/spring.schemas index a75ad5775..6fc4d4247 100644 --- a/spring-batch-core/src/main/resources/META-INF/spring.schemas +++ b/spring-batch-core/src/main/resources/META-INF/spring.schemas @@ -1,4 +1,5 @@ http\://www.springframework.org/schema/batch/spring-batch.xsd=/org/springframework/batch/core/configuration/xml/spring-batch-2.2.xsd http\://www.springframework.org/schema/batch/spring-batch-2.2.xsd=/org/springframework/batch/core/configuration/xml/spring-batch-2.2.xsd http\://www.springframework.org/schema/batch/spring-batch-2.1.xsd=/org/springframework/batch/core/configuration/xml/spring-batch-2.1.xsd -http\://www.springframework.org/schema/batch/spring-batch-2.0.xsd=/org/springframework/batch/core/configuration/xml/spring-batch-2.0.xsd \ No newline at end of file +http\://www.springframework.org/schema/batch/spring-batch-2.0.xsd=/org/springframework/batch/core/configuration/xml/spring-batch-2.0.xsd +http\://xmlns.jcp.org/xml/ns/javaee=/org/springframework/batch/core/jsr/configuration/xml/jobXML_1_0.xsd \ No newline at end of file diff --git a/spring-batch-core/src/main/resources/org/springframework/batch/core/jsr/configuration/xml/jobXML_1_0.xsd b/spring-batch-core/src/main/resources/org/springframework/batch/core/jsr/configuration/xml/jobXML_1_0.xsd new file mode 100755 index 000000000..8ed923d97 --- /dev/null +++ b/spring-batch-core/src/main/resources/org/springframework/batch/core/jsr/configuration/xml/jobXML_1_0.xsd @@ -0,0 +1,435 @@ + + + + + + + Job Specification Language (JSL) specifies a job, + its steps, and directs their execution. + JSL also can be referred to as "Job XML". + + + + + + + This is a helper type. Though it is not otherwise + called out by this name + in the specification, it captures the fact + that the xs:string value refers + to a batch artifact, across numerous + other JSL type definitions. + + + + + + + + + The type of a job definition, whether concrete or + abstract. This is the type of the root element of any JSL document. + + + + + + + The job-level properties, which are accessible + via the JobContext.getProperties() API in a batch artifact. + + + + + + + Note that "listeners" sequence order in XML does + not imply order of execution by + the batch runtime, per the + specification. + + + + + + + + + + + + + + + + + + + The definition of an job, whether concrete or + abstract. This is the + type of the root element of any JSL document. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This grouping provides allows for the reuse of the + 'end', 'fail', 'next', 'stop' element sequences which + may appear at the end of a 'step', 'flow', 'split' or 'decision'. + The term 'TransitionElements' does not formally appear in the spec, it is + a schema convenience. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Note that "listeners" sequence order in XML does + not imply order of execution by + the batch runtime, per the + specification. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Specifies the checkpoint policy that governs + commit behavior for this chunk. + Valid values are: "item" or + "custom". The "item" policy means the + chunk is checkpointed after a + specified number of items are + processed. The "custom" policy means + the chunk is checkpointed + according to a checkpoint algorithm + implementation. Specifying + "custom" requires that the + checkpoint-algorithm element is also + specified. It is an optional + attribute. The default policy is + "item". However, we chose not to define + a schema-specified default for this attribute. + + + + + + + Specifies the number of items to process per chunk + when using the item + checkpoint policy. It must be valid XML integer. + It is an optional + attribute. The default is 10. The item-count + attribute is ignored + for "custom" checkpoint policy. However, to + make it easier for implementations to support JSL inheritance + we + abstain from defining a schema-specified default for this + attribute. + + + + + + + Specifies the amount of time in seconds before + taking a checkpoint for the + item checkpoint policy. It must be valid + XML integer. It is an + optional attribute. The default is 0, which + means no limit. However, to + make it easier for implementations to + support JSL inheritance + we abstain from defining a schema-specified + default for this attribute. + When a value greater than zero is + specified, a checkpoint is taken when + time-limit is reached or + item-count items have been processed, + whichever comes first. The + time-limit attribute is ignored for + "custom" checkpoint policy. + + + + + + + Specifies the number of exceptions a step will + skip if any configured + skippable exceptions are thrown by chunk + processing. It must be a + valid XML integer value. It is an optional + attribute. The default + is no limit. + + + + + + + Specifies the number of times a step will retry if + any configured retryable + exceptions are thrown by chunk processing. + It must be a valid XML + integer value. It is an optional attribute. + The default is no + limit. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/InlineItemHandlerParserTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/InlineItemHandlerParserTests.java index a5a5c6a7e..3ffa5ef2b 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/InlineItemHandlerParserTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/InlineItemHandlerParserTests.java @@ -92,7 +92,6 @@ public class InlineItemHandlerParserTests { Map readers = context.getBeansOfType(ItemReader.class); // Should be 2 each (proxy and target) for the two readers in the steps defined assertEquals(4, readers.size()); - // System.err.println(readers); } } diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/DecisionParsingTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/DecisionParsingTests.java new file mode 100644 index 000000000..0b087d033 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/DecisionParsingTests.java @@ -0,0 +1,44 @@ +package org.springframework.batch.core.jsr.configuration.xml; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.job.flow.FlowExecutionStatus; +import org.springframework.batch.core.job.flow.JobExecutionDecider; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@ContextConfiguration({"DecisionParsingTests-context.xml", "jsr-base-context.xml"}) +@RunWith(SpringJUnit4ClassRunner.class) +public class DecisionParsingTests { + + @Autowired + public Job job; + + @Autowired + public JobLauncher jobLauncher; + + @Test + public void test() throws Exception { + JobExecution execution = jobLauncher.run(job, new JobParameters()); + assertEquals(BatchStatus.COMPLETED, execution.getStatus()); + assertEquals(2, execution.getStepExecutions().size()); + } + + public static class TestDecider implements JobExecutionDecider { + + @Override + public FlowExecutionStatus decide(JobExecution jobExecution, + StepExecution stepExecution) { + return new FlowExecutionStatus("step2"); + } + } +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/ExceptionHandlingParsingTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/ExceptionHandlingParsingTests.java new file mode 100644 index 000000000..16c94b33f --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/ExceptionHandlingParsingTests.java @@ -0,0 +1,93 @@ +package org.springframework.batch.core.jsr.configuration.xml; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@ContextConfiguration({"ExceptionHandlingParsingTests-context.xml", "jsr-base-context.xml"}) +@RunWith(SpringJUnit4ClassRunner.class) +public class ExceptionHandlingParsingTests { + + @Autowired + public Job job; + + @Autowired + public JobLauncher jobLauncher; + + @Test + public void testSkippable() throws Exception { + JobExecution execution1 = jobLauncher.run(job, new JobParametersBuilder().addLong("run", 1l).toJobParameters()); + assertEquals(BatchStatus.FAILED, execution1.getStatus()); + assertEquals(1, execution1.getStepExecutions().size()); + assertEquals(1, execution1.getStepExecutions().iterator().next().getSkipCount()); + assertTrue(execution1.getAllFailureExceptions().get(0).getMessage().contains("But don't skip me")); + + JobExecution execution2 = jobLauncher.run(job, new JobParametersBuilder().addLong("run", 2l).toJobParameters()); + assertEquals(BatchStatus.FAILED, execution2.getStatus()); + assertEquals(2, execution2.getStepExecutions().size()); + assertTrue(execution2.getAllFailureExceptions().get(0).getMessage().contains("But don't retry me")); + + JobExecution execution3 = jobLauncher.run(job, new JobParametersBuilder().addLong("run", 3l).toJobParameters()); + assertEquals(BatchStatus.COMPLETED, execution3.getStatus()); + assertEquals(3, execution3.getStepExecutions().size()); + + List stepExecutions = new ArrayList(execution3.getStepExecutions()); + assertEquals(0, stepExecutions.get(2).getRollbackCount()); + + JobExecution execution4 = jobLauncher.run(job, new JobParametersBuilder().addLong("run", 4l).toJobParameters()); + assertEquals(BatchStatus.COMPLETED, execution4.getStatus()); + assertEquals(3, execution4.getStepExecutions().size()); + } + + public static class ProblemProcessor implements ItemProcessor { + + private long runId = 0; + private boolean hasRetried = false; + + public void setRunId(long id) { + this.runId = id; + } + + @Override + public String process(String item) throws Exception { + throwException(item); + return item; + } + + private void throwException(String item) throws Exception { + if(runId == 1) { + if(item.equals("One")) { + throw new Exception("skip me"); + } else if(item.equals("Two")){ + throw new RuntimeException("But don't skip me"); + } + } else if(runId == 2) { + if(item.equals("Three") && !hasRetried) { + hasRetried = true; + throw new Exception("retry me"); + } else if(item.equals("Four")){ + throw new RuntimeException("But don't retry me"); + } + } else if(runId == 3) { + if(item.equals("Five")) { + throw new Exception("Don't rollback on my account"); + } + } + } + } +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/ItemSkipParsingTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/ItemSkipParsingTests.java new file mode 100644 index 000000000..1228680df --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/ItemSkipParsingTests.java @@ -0,0 +1,178 @@ +package org.springframework.batch.core.jsr.configuration.xml; + +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.SkipListener; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemReader; +import org.springframework.batch.item.ItemWriter; +import org.springframework.batch.item.NonTransientResourceException; +import org.springframework.batch.item.ParseException; +import org.springframework.batch.item.UnexpectedInputException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@ContextConfiguration({"ItemSkipParsingTests-context.xml", "jsr-base-context.xml"}) +@RunWith(SpringJUnit4ClassRunner.class) +public class ItemSkipParsingTests { + + @Autowired + public Job job; + + @Autowired + public JobLauncher jobLauncher; + + @Autowired + public TestSkipListener skipListener; + + @Test + public void test() throws Exception { + // Read skip and fail + JobExecution execution = jobLauncher.run(job, new JobParametersBuilder().toJobParameters()); + + assertEquals(BatchStatus.FAILED, execution.getStatus()); + assertEquals(1, execution.getStepExecutions().iterator().next().getSkipCount()); + assertEquals(1, skipListener.readSkips); + assertEquals(0, skipListener.processSkips); + assertEquals(0, skipListener.writeSkips); + assertEquals("read fail because of me", execution.getAllFailureExceptions().get(0).getCause().getMessage()); + + skipListener.resetCounts(); + + // Process skip and fail + execution = jobLauncher.run(job, new JobParametersBuilder().toJobParameters()); + + assertEquals(BatchStatus.FAILED, execution.getStatus()); + assertEquals(1, execution.getStepExecutions().iterator().next().getSkipCount()); + assertEquals(0, skipListener.readSkips); + assertEquals(1, skipListener.processSkips); + assertEquals(0, skipListener.writeSkips); + assertEquals("process fail because of me", execution.getAllFailureExceptions().get(0).getCause().getMessage()); + + skipListener.resetCounts(); + + // Write skip and fail + execution = jobLauncher.run(job, new JobParametersBuilder().toJobParameters()); + + assertEquals(BatchStatus.FAILED, execution.getStatus()); + assertEquals(1, execution.getStepExecutions().iterator().next().getSkipCount()); + assertEquals(0, skipListener.readSkips); + assertEquals(0, skipListener.processSkips); + assertEquals(1, skipListener.writeSkips); + assertEquals("write fail because of me", execution.getAllFailureExceptions().get(0).getCause().getMessage()); + + skipListener.resetCounts(); + + // Complete + execution = jobLauncher.run(job, new JobParametersBuilder().toJobParameters()); + + assertEquals(BatchStatus.COMPLETED, execution.getStatus()); + assertEquals(0, execution.getStepExecutions().iterator().next().getSkipCount()); + assertEquals(0, skipListener.readSkips); + assertEquals(0, skipListener.processSkips); + assertEquals(0, skipListener.writeSkips); + } + + public static class SkipErrorGeneratingReader implements ItemReader { + private int count = 0; + + @Override + public String read() throws Exception, UnexpectedInputException, + ParseException, NonTransientResourceException { + count++; + + if(count == 1) { + throw new Exception("read skip me"); + } else if (count == 2) { + return "item" + count; + } else if(count == 3) { + throw new RuntimeException("read fail because of me"); + } else if(count < 15) { + return "item" + count; + } else { + return null; + } + } + } + + public static class SkipErrorGeneratingProcessor implements ItemProcessor { + private int count = 0; + + @Override + public String process(String item) throws Exception { + count++; + + if(count == 4) { + throw new Exception("process skip me"); + } else if(count == 5) { + return item; + } else if(count == 6) { + throw new RuntimeException("process fail because of me"); + } else { + return item; + } + } + } + + public static class SkipErrorGeneratingWriter implements ItemWriter { + private int count = 0; + protected List writtenItems = new ArrayList(); + private List skippedItems = new ArrayList(); + + @Override + public void write(List items) throws Exception { + if(items.size() > 0 && !skippedItems.contains(items.get(0))) { + count++; + } + + if(count == 7) { + skippedItems.addAll(items); + throw new Exception("write skip me"); + } else if(count == 9) { + skippedItems = new ArrayList(); + throw new RuntimeException("write fail because of me"); + } else { + writtenItems.addAll(items); + } + } + } + + public static class TestSkipListener implements SkipListener { + + protected int readSkips = 0; + protected int processSkips = 0; + protected int writeSkips = 0; + + @Override + public void onSkipInRead(Throwable t) { + readSkips++; + } + + @Override + public void onSkipInWrite(String item, Throwable t) { + writeSkips++; + } + + @Override + public void onSkipInProcess(String item, Throwable t) { + processSkips++; + } + + public void resetCounts() { + readSkips = 0; + processSkips = 0; + writeSkips = 0; + } + } +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/JobListenerParsingTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/JobListenerParsingTests.java new file mode 100644 index 000000000..ed9f61386 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/JobListenerParsingTests.java @@ -0,0 +1,58 @@ +package org.springframework.batch.core.jsr.configuration.xml; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobExecutionListener; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@ContextConfiguration({"JobListenerParsingTests-context.xml", "jsr-base-context.xml"}) +@RunWith(SpringJUnit4ClassRunner.class) +public class JobListenerParsingTests { + + @Autowired + public Job job; + + @Autowired + public JobLauncher jobLauncher; + + @Autowired + public JobListener listener; + + @Test + public void test() throws Exception { + assertNotNull(job); + assertEquals("job1", job.getName()); + + JobExecution execution = jobLauncher.run(job, new JobParameters()); + assertEquals(BatchStatus.COMPLETED, execution.getStatus()); + assertEquals(2, execution.getStepExecutions().size()); + assertEquals(1, listener.countAfterJob); + assertEquals(1, listener.countBeforeJob); + } + + public static class JobListener implements JobExecutionListener { + + protected int countBeforeJob = 0; + protected int countAfterJob = 0; + + @Override + public void beforeJob(JobExecution jobExecution) { + countBeforeJob++; + } + + @Override + public void afterJob(JobExecution jobExecution) { + countAfterJob++; + } + } +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/SimpleItemBasedJobParsingTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/SimpleItemBasedJobParsingTests.java new file mode 100644 index 000000000..3af73a9fc --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/SimpleItemBasedJobParsingTests.java @@ -0,0 +1,90 @@ +package org.springframework.batch.core.jsr.configuration.xml; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.repeat.CompletionPolicy; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@ContextConfiguration({"SimpleItemBasedJobParsingTests-context.xml", "jsr-base-context.xml"}) +@RunWith(SpringJUnit4ClassRunner.class) +public class SimpleItemBasedJobParsingTests { + + @Autowired + public Job job; + + @Autowired + public Step step1; + + @Autowired + public CountingItemProcessor processor; + + @Autowired + public CountingCompletionPolicy policy; + + @Autowired + public JobLauncher jobLauncher; + + @Test + public void test() throws Exception { + assertNotNull(job); + assertEquals("job1", job.getName()); + assertNotNull(step1); + assertEquals("step1", step1.getName()); + + JobExecution execution = jobLauncher.run(job, new JobParameters()); + assertEquals(BatchStatus.COMPLETED, execution.getStatus()); + assertEquals(3, execution.getStepExecutions().size()); + assertEquals(2, processor.count); + assertEquals(3, policy.counter); + } + + public static class CountingItemProcessor implements ItemProcessor{ + protected int count = 0; + + @Override + public String process(String item) throws Exception { + count++; + return item; + } + } + + public static class CountingCompletionPolicy implements CompletionPolicy { + + protected int counter; + + @Override + public boolean isComplete(RepeatContext context, RepeatStatus result) { + return counter == 3; + } + + @Override + public boolean isComplete(RepeatContext context) { + return counter == 3; + } + + @Override + public RepeatContext start(RepeatContext parent) { + counter = 0; + return parent; + } + + @Override + public void update(RepeatContext context) { + counter++; + } + } +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/SimpleJobParsingTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/SimpleJobParsingTests.java new file mode 100644 index 000000000..7dd622362 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/SimpleJobParsingTests.java @@ -0,0 +1,56 @@ +package org.springframework.batch.core.jsr.configuration.xml; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@ContextConfiguration({"SimpleJobParsingTests-context.xml", "jsr-base-context.xml"}) +@RunWith(SpringJUnit4ClassRunner.class) +public class SimpleJobParsingTests { + + @Autowired + public Job job; + + @Autowired + @Qualifier("step1") + public Step step1; + + @Autowired + @Qualifier("step2") + public Step step2; + + @Autowired + @Qualifier("step3") + public Step step3; + + @Autowired + public JobLauncher jobLauncher; + + @Test + public void test() throws Exception { + assertNotNull(job); + assertEquals("job1", job.getName()); + assertNotNull(step1); + assertEquals("step1", step1.getName()); + assertNotNull(step2); + assertEquals("step2", step2.getName()); + assertNotNull(step3); + assertEquals("step3", step3.getName()); + + JobExecution execution = jobLauncher.run(job, new JobParameters()); + assertEquals(BatchStatus.COMPLETED, execution.getStatus()); + assertEquals(3, execution.getStepExecutions().size()); + } +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/SplitParsingTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/SplitParsingTests.java new file mode 100644 index 000000000..1464cf015 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/SplitParsingTests.java @@ -0,0 +1,53 @@ +package org.springframework.batch.core.jsr.configuration.xml; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@ContextConfiguration({"SplitParsingTests-context.xml", "jsr-base-context.xml"}) +@RunWith(SpringJUnit4ClassRunner.class) +public class SplitParsingTests { + + @Autowired + public Job job; + + @Autowired + public JobLauncher jobLauncher; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void test() throws Exception { + JobExecution execution = jobLauncher.run(job, new JobParameters()); + assertEquals(BatchStatus.COMPLETED, execution.getStatus()); + assertEquals(5, execution.getStepExecutions().size()); + } + + @Test + public void testOneFlowInSplit() { + try { + new ClassPathXmlApplicationContext("/org/springframework/batch/core/jsr/configuration/xml/invalid-split-context.xml"); + } catch (BeanDefinitionParsingException bdpe) { + assertTrue(bdpe.getMessage().indexOf("A must contain at least two 'flow' elements.") >= 0); + return; + } + + fail("Expected exception was not thrown"); + } +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/StepListenerParsingTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/StepListenerParsingTests.java new file mode 100644 index 000000000..c24be99a7 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/StepListenerParsingTests.java @@ -0,0 +1,56 @@ +package org.springframework.batch.core.jsr.configuration.xml; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.StepExecutionListener; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@ContextConfiguration({"StepListenerParsingTests-context.xml", "jsr-base-context.xml"}) +@RunWith(SpringJUnit4ClassRunner.class) +public class StepListenerParsingTests { + + @Autowired + public Job job; + + @Autowired + public JobLauncher jobLauncher; + + @Autowired + public StepListener stepListener; + + @Test + public void test() throws Exception { + JobExecution execution = jobLauncher.run(job, new JobParameters()); + assertEquals(BatchStatus.COMPLETED, execution.getStatus()); + assertEquals(2, execution.getStepExecutions().size()); + assertEquals(2, stepListener.countBeforeStep); + assertEquals(2, stepListener.countAfterStep); + } + + public static class StepListener implements StepExecutionListener { + protected int countBeforeStep = 0; + protected int countAfterStep = 0; + + @Override + public void beforeStep(StepExecution stepExecution) { + countBeforeStep++; + } + + @Override + public ExitStatus afterStep(StepExecution stepExecution) { + countAfterStep++; + return null; + } + } +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/TaskletSupport.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/TaskletSupport.java new file mode 100644 index 000000000..6aa0da2f9 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/TaskletSupport.java @@ -0,0 +1,15 @@ +package org.springframework.batch.core.step.tasklet; + +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.repeat.RepeatStatus; + +public class TaskletSupport implements Tasklet { + + @Override + public RepeatStatus execute(StepContribution contribution, + ChunkContext chunkContext) throws Exception { + System.out.println("The tasklet was executed"); + return RepeatStatus.FINISHED; + } +} diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/DecisionParsingTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/DecisionParsingTests-context.xml new file mode 100644 index 000000000..b54b4db95 --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/DecisionParsingTests-context.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/ExceptionHandlingParsingTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/ExceptionHandlingParsingTests-context.xml new file mode 100644 index 000000000..12a25ca1e --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/ExceptionHandlingParsingTests-context.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/ItemSkipParsingTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/ItemSkipParsingTests-context.xml new file mode 100644 index 000000000..6c5c9919c --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/ItemSkipParsingTests-context.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/JobListenerParsingTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/JobListenerParsingTests-context.xml new file mode 100644 index 000000000..5ef829ec2 --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/JobListenerParsingTests-context.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/SimpleItemBasedJobParsingTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/SimpleItemBasedJobParsingTests-context.xml new file mode 100644 index 000000000..9b8e6eeef --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/SimpleItemBasedJobParsingTests-context.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/SimpleJobParsingTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/SimpleJobParsingTests-context.xml new file mode 100644 index 000000000..36d657924 --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/SimpleJobParsingTests-context.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/SplitParsingTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/SplitParsingTests-context.xml new file mode 100644 index 000000000..308228a2f --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/SplitParsingTests-context.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/StepListenerParsingTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/StepListenerParsingTests-context.xml new file mode 100644 index 000000000..49ea20190 --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/StepListenerParsingTests-context.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/invalid-split-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/invalid-split-context.xml new file mode 100644 index 000000000..17aba5786 --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/invalid-split-context.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/jsr-base-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/jsr-base-context.xml new file mode 100644 index 000000000..6729d10d2 --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/jsr-base-context.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + One + Two + + + + + + + + Three + Four + + + + + + + + Five + Six + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/sample/domain/trade/internal/JdbcTradeWriterTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/sample/domain/trade/internal/JdbcTradeWriterTests.java index d1ce736cc..79dc49731 100644 --- a/spring-batch-samples/src/test/java/org/springframework/batch/sample/domain/trade/internal/JdbcTradeWriterTests.java +++ b/spring-batch-samples/src/test/java/org/springframework/batch/sample/domain/trade/internal/JdbcTradeWriterTests.java @@ -26,6 +26,7 @@ import javax.sql.DataSource; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.batch.sample.domain.trade.Trade; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.jdbc.core.JdbcOperations; @@ -38,12 +39,14 @@ import org.springframework.transaction.annotation.Transactional; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = {"/data-source-context.xml"}) -public class JdbcTradeWriterTests { +public class JdbcTradeWriterTests implements InitializingBean { private JdbcOperations jdbcTemplate; private JdbcTradeDao writer; + private AbstractDataFieldMaxValueIncrementer incrementer; + @Autowired public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); @@ -55,7 +58,7 @@ public class JdbcTradeWriterTests { @Autowired public void setIncrementer(@Qualifier("incrementerParent") AbstractDataFieldMaxValueIncrementer incrementer) { incrementer.setIncrementerName("TRADE_SEQ"); - this.writer.setIncrementer(incrementer); + this.incrementer = incrementer; } @Transactional @Test @@ -69,7 +72,8 @@ public class JdbcTradeWriterTests { writer.writeTrade(trade); - jdbcTemplate.query("SELECT * FROM TRADE WHERE ISIN = '5647238492'", new RowCallbackHandler() { + jdbcTemplate.query("SELECT * FROM TRADE WHERE ISIN = '5647238492'", new RowCallbackHandler() { + @Override public void processRow(ResultSet rs) throws SQLException { assertEquals("testCustomer", rs.getString("CUSTOMER")); assertEquals(new BigDecimal(Double.toString(99.69)), rs.getBigDecimal("PRICE")); @@ -77,4 +81,9 @@ public class JdbcTradeWriterTests { } }); } + + @Override + public void afterPropertiesSet() throws Exception { + this.writer.setIncrementer(incrementer); + } }