From 3bf6bd59da4319168d279b954ebe18534d289db4 Mon Sep 17 00:00:00 2001 From: dsyer Date: Thu, 12 Nov 2009 15:02:45 +0000 Subject: [PATCH] OPEN - issue BATCH-1380: Make a top-level element, so it can be shared or strategised in a job Add at top level and as element in a flow. --- spring-batch-core/.springBeans | 34 +++++- ...lowParser.java => AbstractFlowParser.java} | 42 ++++--- .../xml/CoreNamespaceHandler.java | 1 + .../configuration/xml/DecisionParser.java | 2 +- .../configuration/xml/FlowElementParser.java | 57 ++++++++++ .../configuration/xml/InlineFlowParser.java | 67 +++++++++++ .../configuration/xml/InlineStepParser.java | 2 +- .../core/configuration/xml/JobParser.java | 2 +- .../core/configuration/xml/SplitParser.java | 33 +++--- .../configuration/xml/TopLevelFlowParser.java | 40 +++++++ .../batch/core/job/AbstractJob.java | 22 +++- .../core/job/flow/support/SimpleFlow.java | 3 +- .../job/flow/support/state/FlowState.java | 53 +++++++++ .../configuration/xml/spring-batch-2.1.xsd | 54 +++++++++ .../configuration/xml/FlowJobParserTests.java | 104 ++++++++++++++++++ .../batch/core/job/SimpleJobTests.java | 22 ++++ .../batch/core/job/flow/FlowJobTests.java | 50 +++++---- .../xml/FlowJobParserTests-context.xml | 40 +++++++ 18 files changed, 573 insertions(+), 55 deletions(-) rename spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/{FlowParser.java => AbstractFlowParser.java} (94%) create mode 100644 spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/FlowElementParser.java create mode 100644 spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/InlineFlowParser.java create mode 100644 spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/TopLevelFlowParser.java create mode 100644 spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/support/state/FlowState.java create mode 100644 spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/FlowJobParserTests.java create mode 100644 spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/FlowJobParserTests-context.xml diff --git a/spring-batch-core/.springBeans b/spring-batch-core/.springBeans index 7e376f2b9..406e7e463 100644 --- a/spring-batch-core/.springBeans +++ b/spring-batch-core/.springBeans @@ -1,7 +1,7 @@ 1 - + @@ -62,6 +62,38 @@ src/test/resources/org/springframework/batch/core/configuration/xml/StopCustomStatusJobParserTests-context.xml src/test/resources/org/springframework/batch/core/scope/util/PlaceholderTargetSourceCustomEditorTests-context.xml src/test/resources/org/springframework/batch/core/scope/util/PlaceholderTargetSourceErrorTests-context.xml + src/test/resources/org/springframework/batch/core/configuration/support/child-context.xml + src/test/resources/org/springframework/batch/core/configuration/xml/ChunkElementParentAttributeParserTests-context.xml + src/test/resources/org/springframework/batch/core/configuration/support/ClassPathXmlJobLoaderContextTests-context.xml + src/test/resources/org/springframework/batch/core/configuration/xml/DuplicateTransitionJobParserTests-context.xml + src/test/resources/org/springframework/batch/core/launch/support/error.xml + src/test/resources/org/springframework/batch/core/step/item/FaultTolerantExceptionClassesTests-context.xml + src/test/resources/org/springframework/batch/core/configuration/xml/FlowJobParserTests-context.xml + src/test/resources/org/springframework/batch/core/configuration/xml/InlineItemHandlerParserTests-context.xml + src/test/resources/org/springframework/batch/core/configuration/xml/JobParserNextOutOfScopeTests-context.xml + src/test/resources/org/springframework/batch/core/configuration/xml/JobParserParentAttributeTests-context.xml + src/test/resources/org/springframework/batch/core/configuration/xml/JobParserUnreachableStepInFlowTests-context.xml + src/test/resources/org/springframework/batch/core/configuration/xml/JobParserUnreachableStepTests-context.xml + src/test/resources/org/springframework/batch/core/configuration/xml/JobRepositoryParserReferenceTests-context.xml + src/test/resources/org/springframework/batch/core/launch/support/launcher-with-environment.xml + src/test/resources/org/springframework/batch/core/launch/support/launcher-with-locator.xml + src/test/resources/org/springframework/batch/core/resource/ListPreparedStatementSetterTests-context.xml + src/test/resources/org/springframework/batch/core/configuration/xml/NextAttributeMultipleFinalJobParserTests-context.xml + src/test/resources/org/springframework/batch/core/configuration/support/parent-context.xml + src/test/resources/org/springframework/batch/core/repository/support/SimpleJobRepositoryProxyTests-context.xml + src/test/resources/org/springframework/batch/core/configuration/xml/SplitInterruptedJobParserTests-context.xml + src/test/resources/org/springframework/batch/core/configuration/xml/StepListenerParserTests-context.xml + src/test/resources/org/springframework/batch/core/configuration/xml/StepParserBadRetryListenerTests-context.xml + src/test/resources/org/springframework/batch/core/configuration/xml/StepParserBadStepListenerTests-context.xml + src/test/resources/org/springframework/batch/core/configuration/xml/StepParserCommitIntervalCompletionPolicyTests-context.xml + src/test/resources/org/springframework/batch/core/configuration/xml/StepParserCommitIntervalTests-context.xml + src/test/resources/org/springframework/batch/core/configuration/xml/StepParserCompletionPolicyTests-context.xml + src/test/resources/org/springframework/batch/core/configuration/xml/StepParserNoCommitIntervalOrCompletionPolicyTests-context.xml + src/test/resources/org/springframework/batch/core/configuration/xml/StepParserParentAttributeTests-context.xml + src/test/resources/org/springframework/batch/core/scope/StepScopePerformanceTests-context.xml + src/test/resources/org/springframework/batch/core/scope/StepScopeProxyTargetClassIntegrationTests-context.xml + src/test/resources/org/springframework/batch/core/configuration/xml/StopAndRestartFailedJobParserTests-context.xml + src/test/resources/org/springframework/batch/core/launch/support/test-environment-with-registry-and-auto-register.xml diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/FlowParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/AbstractFlowParser.java similarity index 94% rename from spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/FlowParser.java rename to spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/AbstractFlowParser.java index 40b63fdfe..58a09109c 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/FlowParser.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/AbstractFlowParser.java @@ -42,12 +42,14 @@ import org.w3c.dom.NodeList; * @author Dave Syer * */ -public class FlowParser extends AbstractSingleBeanDefinitionParser { +public abstract class AbstractFlowParser extends AbstractSingleBeanDefinitionParser { private static final String ID_ATTR = "id"; private static final String STEP_ELE = "step"; + private static final String FLOW_ELE = "flow"; + private static final String DECISION_ELE = "decision"; private static final String SPLIT_ELE = "split"; @@ -72,27 +74,36 @@ public class FlowParser extends AbstractSingleBeanDefinitionParser { private static final InlineStepParser stepParser = new InlineStepParser(); + private static final FlowElementParser flowParser = new FlowElementParser(); + private static final DecisionParser decisionParser = new DecisionParser(); // For generating unique state names for end transitions private static int endCounter = 0; - private final String flowName; + private String flowName; - private final String jobFactoryRef; + private String jobFactoryRef; /** - * Construct a {@link FlowParser} with the specified name and using the - * provided job repository ref. + * Convenience method for subclasses to set up the flow name for error + * reporting. * - * @param flowName the name of the flow - * @param jobFactoryRef the reference to the {@link JobParserJobFactoryBean} - * from the enclosing tag + * @param flowName */ - public FlowParser(String flowName, String jobFactoryRef) { + protected void setFlowName(String flowName) { this.flowName = flowName; - this.jobFactoryRef = jobFactoryRef; + } + /** + * Convenience method for subclasses to set the job factory reference if it + * is available (null is fine, but the quality of error reports is better if + * it is available). + * + * @param jobFactoryRef + */ + protected void setJobFactoryRef(String jobFactoryRef) { + this.jobFactoryRef = jobFactoryRef; } /* @@ -111,6 +122,7 @@ public class FlowParser extends AbstractSingleBeanDefinitionParser { */ @Override protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + List stateTransitions = new ArrayList(); SplitParser splitParser = new SplitParser(jobFactoryRef); @@ -134,6 +146,10 @@ public class FlowParser extends AbstractSingleBeanDefinitionParser { else if (nodeName.equals(DECISION_ELE)) { stateTransitions.addAll(decisionParser.parse(child, parserContext)); } + else if (nodeName.equals(FLOW_ELE)) { + stateTransitions.addAll(flowParser.parse(child, parserContext)); + stepExists = true; + } else if (nodeName.equals(SPLIT_ELE)) { stateTransitions.addAll(splitParser .parse(child, new ParserContext(parserContext.getReaderContext(), parserContext @@ -141,7 +157,7 @@ public class FlowParser extends AbstractSingleBeanDefinitionParser { stepExists = true; } - if (Arrays.asList(STEP_ELE, DECISION_ELE, SPLIT_ELE).contains(nodeName)) { + if (Arrays.asList(STEP_ELE, DECISION_ELE, SPLIT_ELE, FLOW_ELE).contains(nodeName)) { reachableElementMap.put(child.getAttribute(ID_ATTR), findReachableElements(child)); if (startElement == null) { startElement = child.getAttribute(ID_ATTR); @@ -170,10 +186,6 @@ public class FlowParser extends AbstractSingleBeanDefinitionParser { boolean dummy = managedList.addAll(stateTransitions); builder.addPropertyValue("stateTransitions", managedList); - builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - - parserContext.popAndRegisterContainingComponent(); - } /** diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespaceHandler.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespaceHandler.java index 9765c9fe0..b09bb7b1c 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespaceHandler.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespaceHandler.java @@ -31,6 +31,7 @@ public class CoreNamespaceHandler extends NamespaceHandlerSupport { */ public void init() { this.registerBeanDefinitionParser("job", new JobParser()); + this.registerBeanDefinitionParser("flow", new TopLevelFlowParser()); this.registerBeanDefinitionParser("step", new TopLevelStepParser()); this.registerBeanDefinitionParser("job-repository", new JobRepositoryParser()); this.registerBeanDefinitionParser("job-listener", new TopLevelJobListenerParser()); diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/DecisionParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/DecisionParser.java index fb97b1ea2..5fa21668c 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/DecisionParser.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/DecisionParser.java @@ -55,7 +55,7 @@ public class DecisionParser { 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); + return InlineFlowParser.getNextElements(parserContext, stateBuilder.getBeanDefinition(), element); } } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/FlowElementParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/FlowElementParser.java new file mode 100644 index 000000000..cfa0be742 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/FlowElementParser.java @@ -0,0 +1,57 @@ +/* + * Copyright 2006-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.batch.core.configuration.xml; + +import java.util.Collection; + +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; + +/** + * Internal parser for the <flow/> elements inside a job.. + * + * @see JobParser + * + * @author Dave Syer + * + */ +public class FlowElementParser { + + /** + * Parse the flow and turn it into a list of transitions. + * + * @param element the <flow/gt; element to parse + * @param parserContext the parser context for the bean factory + * @return a collection of bean definitions for + * {@link org.springframework.batch.core.job.flow.support.StateTransition} + * instances objects + */ + public Collection parse(Element element, ParserContext parserContext) { + + String refAttribute = element.getAttribute("ref"); + String idAttribute = element.getAttribute("id"); + + BeanDefinitionBuilder stateBuilder = + BeanDefinitionBuilder.genericBeanDefinition("org.springframework.batch.core.job.flow.support.state.FlowState"); + stateBuilder.addConstructorArgValue(new RuntimeBeanReference(refAttribute)); + stateBuilder.addConstructorArgValue(idAttribute); + return InlineFlowParser.getNextElements(parserContext, stateBuilder.getBeanDefinition(), element); + + } +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/InlineFlowParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/InlineFlowParser.java new file mode 100644 index 000000000..23f680e2e --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/InlineFlowParser.java @@ -0,0 +1,67 @@ +/* + * Copyright 2006-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.batch.core.configuration.xml; + +import org.springframework.batch.core.job.flow.support.SimpleFlow; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.ParserContext; +import org.w3c.dom.Element; + +/** + * @author Dave Syer + * + */ +public class InlineFlowParser extends AbstractFlowParser { + + /** + * Construct a {@link InlineFlowParser} with the specified name and using the + * provided job repository ref. + * + * @param flowName the name of the flow + * @param jobFactoryRef the reference to the {@link JobParserJobFactoryBean} + * from the enclosing tag + */ + public InlineFlowParser(String flowName, String jobFactoryRef) { + setFlowName(flowName); + setJobFactoryRef(jobFactoryRef); + + } + + /* + * (non-Javadoc) + * + * @see AbstractSingleBeanDefinitionParser#getBeanClass(Element) + */ + @Override + protected Class getBeanClass(Element element) { + return SimpleFlow.class; + } + + /** + * @param element the top level element containing a flow definition + * @param parserContext the {@link ParserContext} + */ + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + + super.doParse(element, parserContext, builder); + builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + parserContext.popAndRegisterContainingComponent(); + + } + +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/InlineStepParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/InlineStepParser.java index ef5a28a0a..75b4532d0 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/InlineStepParser.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/InlineStepParser.java @@ -60,7 +60,7 @@ public class InlineStepParser extends AbstractStepParser { parserContext.registerBeanComponent(new BeanComponentDefinition(bd, stepId)); stateBuilder.addConstructorArgReference(stepId); - return FlowParser.getNextElements(parserContext, stepId, stateBuilder.getBeanDefinition(), element); + return InlineFlowParser.getNextElements(parserContext, stepId, stateBuilder.getBeanDefinition(), element); } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/JobParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/JobParser.java index 8d9fec2da..e08c2d3eb 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/JobParser.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/JobParser.java @@ -101,7 +101,7 @@ public class JobParser extends AbstractSingleBeanDefinitionParser { } } else { - FlowParser flowParser = new FlowParser(jobName, jobName); + InlineFlowParser flowParser = new InlineFlowParser(jobName, jobName); BeanDefinition flowDef = flowParser.parse(element, parserContext); builder.addPropertyValue("flow", flowDef); } 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 c96ec1bfb..499d9ff5c 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 @@ -15,7 +15,6 @@ */ package org.springframework.batch.core.configuration.xml; -import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -45,10 +44,11 @@ public class SplitParser { private final String jobFactoryRef; /** - * Construct a {@link FlowParser} using the provided job repository ref. + * Construct a {@link InlineFlowParser} using the provided job repository + * ref. * * @param jobFactoryRef the reference to the {@link JobParserJobFactoryBean} - * from the enclosing tag + * from the enclosing tag */ public SplitParser(String jobFactoryRef) { this.jobFactoryRef = jobFactoryRef; @@ -60,8 +60,8 @@ public class SplitParser { * @param element the <split/gt; element to parse * @param parserContext the parser context for the bean factory * @return a collection of bean definitions for - * {@link org.springframework.batch.core.job.flow.support.StateTransition} - * instances objects + * {@link org.springframework.batch.core.job.flow.support.StateTransition} + * instances objects */ public Collection parse(Element element, ParserContext parserContext) { @@ -83,21 +83,28 @@ public class SplitParser { parserContext.getReaderContext().error("A must contain at least two 'flow' elements.", element); } - Collection flows = new ArrayList(); + @SuppressWarnings("unchecked") + Collection flows = new ManagedList(); int i = 0; for (Element nextElement : flowElements) { - FlowParser flowParser = new FlowParser(idAttribute + "#" + i, jobFactoryRef); - flows.add(flowParser.parse(nextElement, parserContext)); + InlineFlowParser flowParser = new InlineFlowParser(idAttribute + "#" + i, jobFactoryRef); + String ref = nextElement.getAttribute("ref"); + if (StringUtils.hasText(ref)) { + if (nextElement.getElementsByTagName("*").getLength() > 0) { + parserContext.getReaderContext().error("A in a must have ref= or nested , but not both.", nextElement); + } + flows.add(new RuntimeBeanReference(ref)); + } + else { + flows.add(flowParser.parse(nextElement, parserContext)); + } i++; } - ManagedList managedList = new ManagedList(); - @SuppressWarnings( { "unchecked", "unused" }) - boolean dummy = managedList.addAll(flows); - stateBuilder.addConstructorArgValue(managedList); + stateBuilder.addConstructorArgValue(flows); stateBuilder.addConstructorArgValue(idAttribute); - return FlowParser.getNextElements(parserContext, stateBuilder.getBeanDefinition(), element); + return InlineFlowParser.getNextElements(parserContext, stateBuilder.getBeanDefinition(), element); } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/TopLevelFlowParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/TopLevelFlowParser.java new file mode 100644 index 000000000..68660e5e5 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/TopLevelFlowParser.java @@ -0,0 +1,40 @@ +/* + * Copyright 2006-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.batch.core.configuration.xml; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.ParserContext; +import org.w3c.dom.Element; + +/** + * @author Dave Syer + * + */ +public class TopLevelFlowParser extends AbstractFlowParser { + + private static final String ID_ATTR = "id"; + + /** + * @param element the top level element containing a flow definition + * @param parserContext the {@link ParserContext} + */ + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + setFlowName(element.getAttribute(ID_ATTR)); + super.doParse(element, parserContext, builder); + } + +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java index 9e9470337..bce32b9dd 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java @@ -347,6 +347,13 @@ public abstract class AbstractJob implements Job, StepLocator, BeanNameAware, In JobInstance jobInstance = execution.getJobInstance(); StepExecution lastStepExecution = jobRepository.getLastStepExecution(jobInstance, step.getName()); + if (stepExecutionPartOfExistingJobExecution(execution, lastStepExecution)) { + // If the last execution of this step was in the same job, it's probably + // intentional so we want to run it again... + logger.info(String.format("Duplicate step [%s] detected in execution of job=[%s]. " + + "If either step fails, both will be executed again on restart.", step.getName(), name)); + lastStepExecution = null; + } StepExecution currentStepExecution = lastStepExecution; if (shouldStart(lastStepExecution, jobInstance, step)) { @@ -395,6 +402,17 @@ public abstract class AbstractJob implements Job, StepLocator, BeanNameAware, In } + /** + * Detect whether a step execution belongs to this job execution. + * @param jobExecution the current job execution + * @param stepExecution an existing step execution + * @return + */ + private boolean stepExecutionPartOfExistingJobExecution(JobExecution jobExecution, StepExecution stepExecution) { + return stepExecution != null && stepExecution.getJobExecutionId() != null + && stepExecution.getJobExecutionId().equals(jobExecution.getId()); + } + /** * Convenience method for subclasses so they can change the state of a * {@link StepExecution} if necessary. Use with care (and not at all @@ -430,9 +448,9 @@ public abstract class AbstractJob implements Job, StepLocator, BeanNameAware, In } if (stepStatus == BatchStatus.UNKNOWN) { - throw new JobRestartException("Cannot restart step from UNKNOWN status. " + throw new JobRestartException("Cannot restart step from UNKNOWN status. " + "The last execution ended with a failure that could not be rolled back, " - + "so it may be dangerous to proceed. " + "Manual intervention is probably necessary."); + + "so it may be dangerous to proceed. Manual intervention is probably necessary."); } if ((stepStatus == BatchStatus.COMPLETED && step.isAllowStartIfComplete() == false) diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/support/SimpleFlow.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/support/SimpleFlow.java index 5165230b6..e3662dc26 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/support/SimpleFlow.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/support/SimpleFlow.java @@ -221,7 +221,8 @@ public class SimpleFlow implements Flow, InitializingBean { for (StateTransition stateTransition : stateTransitions) { State state = stateTransition.getState(); - stateMap.put(state.getName(), state); + String stateName = state.getName(); + stateMap.put(stateName, state); } for (StateTransition stateTransition : stateTransitions) { diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/support/state/FlowState.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/support/state/FlowState.java new file mode 100644 index 000000000..c173e5d33 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/support/state/FlowState.java @@ -0,0 +1,53 @@ +/* + * Copyright 2006-2007 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.job.flow.support.state; + +import org.springframework.batch.core.job.flow.Flow; +import org.springframework.batch.core.job.flow.FlowExecutionStatus; +import org.springframework.batch.core.job.flow.FlowExecutor; + +/** + * State that delegates to a Flow + * + * @author Dave Syer + * @since 2.0 + */ +public class FlowState extends AbstractState { + + private final Flow flow; + + /** + * @param name + */ + public FlowState(Flow flow, String name) { + super(name); + this.flow = flow; + } + + @Override + public FlowExecutionStatus handle(FlowExecutor executor) throws Exception { + return flow.start(executor).getStatus(); + } + + /* (non-Javadoc) + * @see org.springframework.batch.core.job.flow.State#isEndState() + */ + public boolean isEndState() { + return false; + } + +} \ No newline at end of file diff --git a/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.1.xsd b/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.1.xsd index 021d86130..99c682b53 100644 --- a/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.1.xsd +++ b/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.1.xsd @@ -127,6 +127,24 @@ + + + + Defines a flow composed of a set of steps and + transitions between steps. + + + + + + + + + + + + + @@ -295,6 +313,18 @@ + + + + + + + + + + @@ -318,6 +348,30 @@ + + + + Declares job should include an externalized flow here. + + + + + + + + + + + + + + + + + + diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/FlowJobParserTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/FlowJobParserTests.java new file mode 100644 index 000000000..afc45493c --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/FlowJobParserTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.batch.core.configuration.xml; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Before; +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.repository.JobRepository; +import org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean; +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; + + +/** + * @author Dave Syer + * + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class FlowJobParserTests { + + @Autowired + @Qualifier("job1") + private Job job1; + + @Autowired + @Qualifier("job2") + private Job job2; + + @Autowired + @Qualifier("job3") + private Job job3; + + @Autowired + @Qualifier("job4") + private Job job4; + + @Autowired + private JobRepository jobRepository; + + @Before + public void setUp() { + MapJobRepositoryFactoryBean.clear(); + } + + @Test + public void testFlowJob() throws Exception { + assertNotNull(job1); + JobExecution jobExecution = jobRepository.createJobExecution(job1.getName(), new JobParameters()); + job1.execute(jobExecution); + assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus()); + assertEquals(4, jobExecution.getStepExecutions().size()); + } + + @Test + public void testFlowJobWithNestedTransitions() throws Exception { + assertNotNull(job2); + JobExecution jobExecution = jobRepository.createJobExecution(job2.getName(), new JobParameters()); + job2.execute(jobExecution); + assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus()); + assertEquals(3, jobExecution.getStepExecutions().size()); + } + + @Test + public void testFlowJobWithNoSteps() throws Exception { + assertNotNull(job3); + JobExecution jobExecution = jobRepository.createJobExecution(job3.getName(), new JobParameters()); + job3.execute(jobExecution); + assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus()); + assertEquals(2, jobExecution.getStepExecutions().size()); + } + + @Test + public void testFlowInSplit() throws Exception { + assertNotNull(job4); + JobExecution jobExecution = jobRepository.createJobExecution(job4.getName(), new JobParameters()); + job4.execute(jobExecution); + assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus()); + assertEquals(4, jobExecution.getStepExecutions().size()); + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/job/SimpleJobTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/job/SimpleJobTests.java index 2e209442e..3a3205094 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/job/SimpleJobTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/job/SimpleJobTests.java @@ -30,6 +30,7 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Date; import java.util.List; import org.junit.Before; @@ -325,12 +326,32 @@ public class SimpleJobTests { public void testStepAlreadyComplete() throws Exception { stepExecution1.setStatus(BatchStatus.COMPLETED); jobRepository.add(stepExecution1); + jobExecution.setEndTime(new Date()); + jobRepository.update(jobExecution); + jobExecution = jobRepository.createJobExecution(job.getName(), jobParameters); job.execute(jobExecution); assertEquals(0, jobExecution.getFailureExceptions().size()); assertEquals(1, jobExecution.getStepExecutions().size()); assertEquals(stepExecution2.getStepName(), jobExecution.getStepExecutions().iterator().next().getStepName()); } + @Test + public void testStepAlreadyCompleteInSameExecution() throws Exception { + List steps = new ArrayList(); + steps.add(step1); + steps.add(step2); + // Two steps with the same name should both be executed, since + // the user might actually want it to happen twice. On a restart + // it would be executed twice again, even if it failed on the + // second execution. This seems reasonable. + steps.add(step2); + job.setSteps(steps); + job.execute(jobExecution); + assertEquals(0, jobExecution.getFailureExceptions().size()); + assertEquals(3, jobExecution.getStepExecutions().size()); + assertEquals(stepExecution1.getStepName(), jobExecution.getStepExecutions().iterator().next().getStepName()); + } + @Test public void testNoSteps() throws Exception { job.setSteps(new ArrayList()); @@ -362,6 +383,7 @@ public class SimpleJobTests { Throwable e = jobExecution.getAllFailureExceptions().get(0); assertSame(exception, e); + jobExecution = jobRepository.createJobExecution(job.getName(), jobParameters); job.execute(jobExecution); e = jobExecution.getAllFailureExceptions().get(0); assertSame(exception, e); diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/job/flow/FlowJobTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/job/flow/FlowJobTests.java index 7f6a2bec6..f1badc495 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/job/flow/FlowJobTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/job/flow/FlowJobTests.java @@ -100,9 +100,10 @@ public class FlowJobTests { SimpleFlow flow = new SimpleFlow("job"); List transitions = new ArrayList(); transitions.add(StateTransition.createStateTransition(new StepState(new StubStep("step1")), "step2")); - transitions.add(StateTransition.createStateTransition(new StepState(new StubStep("step2")), ExitStatus.FAILED + StepState step2 = new StepState(new StubStep("step2")); + transitions.add(StateTransition.createStateTransition(step2, ExitStatus.FAILED .getExitCode(), "end0")); - transitions.add(StateTransition.createStateTransition(new StepState(new StubStep("step2")), + transitions.add(StateTransition.createStateTransition(step2, ExitStatus.COMPLETED.getExitCode(), "end1")); transitions.add(StateTransition.createEndStateTransition(new EndState(FlowExecutionStatus.FAILED, "end0"))); transitions.add(StateTransition.createEndStateTransition(new EndState(FlowExecutionStatus.COMPLETED, "end1"))); @@ -121,9 +122,10 @@ public class FlowJobTests { List transitions = new ArrayList(); transitions.add(StateTransition.createStateTransition(new StateSupport("step1", FlowExecutionStatus.FAILED), "step2")); - transitions.add(StateTransition.createStateTransition(new StepState(new StubStep("step2")), ExitStatus.FAILED + StepState step2 = new StepState(new StubStep("step2")); + transitions.add(StateTransition.createStateTransition(step2, ExitStatus.FAILED .getExitCode(), "end0")); - transitions.add(StateTransition.createStateTransition(new StepState(new StubStep("step2")), + transitions.add(StateTransition.createStateTransition(step2, ExitStatus.COMPLETED.getExitCode(), "end1")); transitions.add(StateTransition.createEndStateTransition(new EndState(FlowExecutionStatus.FAILED, "end0"))); transitions.add(StateTransition.createEndStateTransition(new EndState(FlowExecutionStatus.COMPLETED, "end1"))); @@ -328,9 +330,10 @@ public class FlowJobTests { transitions.add(StateTransition.createStateTransition(new StepState(new StubStep("step1")), "end")); transitions.add(StateTransition .createStateTransition(new EndState(FlowExecutionStatus.STOPPED, "end"), "step2")); - transitions.add(StateTransition.createStateTransition(new StepState(new StubStep("step2")), ExitStatus.FAILED + StepState step2 = new StepState(new StubStep("step2")); + transitions.add(StateTransition.createStateTransition(step2, ExitStatus.FAILED .getExitCode(), "end0")); - transitions.add(StateTransition.createStateTransition(new StepState(new StubStep("step2")), + transitions.add(StateTransition.createStateTransition(step2, ExitStatus.COMPLETED.getExitCode(), "end1")); transitions.add(StateTransition.createEndStateTransition(new EndState(FlowExecutionStatus.FAILED, "end0"))); transitions.add(StateTransition.createEndStateTransition(new EndState(FlowExecutionStatus.COMPLETED, "end1"))); @@ -369,9 +372,10 @@ public class FlowJobTests { transitions.add(StateTransition.createStateTransition(new StepState(new StubStep("step1")), "end")); transitions.add(StateTransition .createStateTransition(new EndState(FlowExecutionStatus.STOPPED, "end"), "step2")); - transitions.add(StateTransition.createStateTransition(new StepState(new StubStep("step2")), + StepState step2 = new StepState(new StubStep("step2")); + transitions.add(StateTransition.createStateTransition(step2, ExitStatus.COMPLETED.getExitCode(), "end0")); - transitions.add(StateTransition.createStateTransition(new StepState(new StubStep("step2")), ExitStatus.FAILED + transitions.add(StateTransition.createStateTransition(step2, ExitStatus.FAILED .getExitCode(), "end1")); transitions.add(StateTransition.createEndStateTransition(new EndState(FlowExecutionStatus.COMPLETED, "end0"))); transitions.add(StateTransition.createEndStateTransition(new EndState(FlowExecutionStatus.FAILED, "end1"))); @@ -395,18 +399,21 @@ public class FlowJobTests { public void testBranching() throws Exception { SimpleFlow flow = new SimpleFlow("job"); List transitions = new ArrayList(); - transitions.add(StateTransition.createStateTransition(new StepState(new StubStep("step1")), "step2")); - transitions.add(StateTransition.createStateTransition(new StepState(new StubStep("step1")), "COMPLETED", + StepState step1 = new StepState(new StubStep("step1")); + transitions.add(StateTransition.createStateTransition(step1, "step2")); + transitions.add(StateTransition.createStateTransition(step1, "COMPLETED", "step3")); - transitions.add(StateTransition.createStateTransition(new StepState(new StubStep("step2")), + StepState step2 = new StepState(new StubStep("step2")); + transitions.add(StateTransition.createStateTransition(step2, ExitStatus.COMPLETED.getExitCode(), "end0")); - transitions.add(StateTransition.createStateTransition(new StepState(new StubStep("step2")), ExitStatus.FAILED + transitions.add(StateTransition.createStateTransition(step2, ExitStatus.FAILED .getExitCode(), "end1")); transitions.add(StateTransition.createEndStateTransition(new EndState(FlowExecutionStatus.COMPLETED, "end0"))); transitions.add(StateTransition.createEndStateTransition(new EndState(FlowExecutionStatus.FAILED, "end1"))); - transitions.add(StateTransition.createStateTransition(new StepState(new StubStep("step3")), ExitStatus.FAILED + StepState step3 = new StepState(new StubStep("step3")); + transitions.add(StateTransition.createStateTransition(step3, ExitStatus.FAILED .getExitCode(), "end2")); - transitions.add(StateTransition.createStateTransition(new StepState(new StubStep("step3")), + transitions.add(StateTransition.createStateTransition(step3, ExitStatus.COMPLETED.getExitCode(), "end3")); transitions.add(StateTransition.createEndStateTransition(new EndState(FlowExecutionStatus.FAILED, "end2"))); transitions.add(StateTransition.createEndStateTransition(new EndState(FlowExecutionStatus.COMPLETED, "end3"))); @@ -447,18 +454,21 @@ public class FlowJobTests { List transitions = new ArrayList(); transitions.add(StateTransition.createStateTransition(new StepState(new StubStep("step1")), "decision")); - transitions.add(StateTransition.createStateTransition(new DecisionState(decider, "decision"), "step2")); + DecisionState decision = new DecisionState(decider, "decision"); + transitions.add(StateTransition.createStateTransition(decision, "step2")); transitions.add(StateTransition - .createStateTransition(new DecisionState(decider, "decision"), "SWITCH", "step3")); - transitions.add(StateTransition.createStateTransition(new StepState(new StubStep("step2")), + .createStateTransition(decision, "SWITCH", "step3")); + StepState step2 = new StepState(new StubStep("step2")); + transitions.add(StateTransition.createStateTransition(step2, ExitStatus.COMPLETED.getExitCode(), "end0")); - transitions.add(StateTransition.createStateTransition(new StepState(new StubStep("step2")), ExitStatus.FAILED + transitions.add(StateTransition.createStateTransition(step2, ExitStatus.FAILED .getExitCode(), "end1")); transitions.add(StateTransition.createEndStateTransition(new EndState(FlowExecutionStatus.COMPLETED, "end0"))); transitions.add(StateTransition.createEndStateTransition(new EndState(FlowExecutionStatus.FAILED, "end1"))); - transitions.add(StateTransition.createStateTransition(new StepState(new StubStep("step3")), ExitStatus.FAILED + StepState step3 = new StepState(new StubStep("step3")); + transitions.add(StateTransition.createStateTransition(step3, ExitStatus.FAILED .getExitCode(), "end2")); - transitions.add(StateTransition.createStateTransition(new StepState(new StubStep("step3")), + transitions.add(StateTransition.createStateTransition(step3, ExitStatus.COMPLETED.getExitCode(), "end3")); transitions.add(StateTransition.createEndStateTransition(new EndState(FlowExecutionStatus.FAILED, "end2"))); transitions.add(StateTransition.createEndStateTransition(new EndState(FlowExecutionStatus.COMPLETED, "end3"))); diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/FlowJobParserTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/FlowJobParserTests-context.xml new file mode 100644 index 000000000..dfd781e84 --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/FlowJobParserTests-context.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file