diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/builder/FlowBuilder.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/builder/FlowBuilder.java index dd06f9a97..f4e5e157f 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/job/builder/FlowBuilder.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/builder/FlowBuilder.java @@ -42,13 +42,13 @@ import org.springframework.core.task.TaskExecutor; /** * A builder for a flow of steps that can be executed as a job or as part of a job. Steps can be linked together with * conditional transitions that depend on the exit status of the previous step. - * + * * @author Dave Syer - * + * * @since 2.2 - * + * * @param the type of object returned by the builder (by default a Flow) - * + * */ public class FlowBuilder { @@ -66,10 +66,14 @@ public class FlowBuilder { private EndState completedState; + private EndState stoppedState; + private int decisionCounter = 0; private int splitCounter = 0; + private int endCounter = 0; + private Map states = new HashMap(); private SimpleFlow flow; @@ -81,12 +85,13 @@ public class FlowBuilder { this.prefix = name + "."; this.failedState = new EndState(FlowExecutionStatus.FAILED, prefix + "FAILED"); this.completedState = new EndState(FlowExecutionStatus.COMPLETED, prefix + "COMPLETED"); + this.stoppedState = new EndState(FlowExecutionStatus.STOPPED, prefix + "STOPPED"); } /** * Validate the current state of the builder and build a flow. Subclasses may override this to build an object of a * different type that itself depends on the flow. - * + * * @return a flow */ public Q build() { @@ -98,7 +103,7 @@ public class FlowBuilder { /** * Transition to the next step on successful completion of the current step. All other outcomes are treated as * failures. - * + * * @param step the next step * @return this to enable chaining */ @@ -109,7 +114,7 @@ public class FlowBuilder { /** * Start a flow. If some steps are already registered, just a synonym for {@link #from(Step)}. - * + * * @param step the step to start with * @return this to enable chaining */ @@ -121,7 +126,7 @@ public class FlowBuilder { /** * Go back to a previously registered step and start a new path. If no steps are registered yet just a synonym for * {@link #start(Step)}. - * + * * @param step the step to start from (already registered) * @return this to enable chaining */ @@ -133,8 +138,8 @@ public class FlowBuilder { /** * Transition to the decider on successful completion of the current step. All other outcomes are treated as * failures. - * - * @param step the next step + * + * @param decider the JobExecutionDecider to determine the next step to execute * @return this to enable chaining */ public UnterminatedFlowBuilder next(JobExecutionDecider decider) { @@ -144,7 +149,7 @@ public class FlowBuilder { /** * If a flow should start with a decision use this as the first state. - * + * * @param decider the to start from * @return a builder to enable chaining */ @@ -155,7 +160,7 @@ public class FlowBuilder { /** * Start again from a decision that was already registered. - * + * * @param decider the decider to start from (already registered) * @return a builder to enable chaining */ @@ -166,7 +171,7 @@ public class FlowBuilder { /** * Go next on successful completion to a subflow. - * + * * @param flow the flow to go to * @return a builder to enable chaining */ @@ -177,7 +182,7 @@ public class FlowBuilder { /** * Start again from a subflow that was already registered. - * + * * @param flow the flow to start from (already registered) * @return a builder to enable chaining */ @@ -188,7 +193,7 @@ public class FlowBuilder { /** * If a flow should start with a subflow use this as the first state. - * + * * @param flow the flow to start from * @return a builder to enable chaining */ @@ -209,7 +214,7 @@ public class FlowBuilder { * Start a transition to a new state if the exit status from the previous state matches the pattern given. * Successful completion normally results in an exit status equal to (or starting with by convention) "COMPLETED". * See {@link ExitStatus} for commonly used values. - * + * * @param pattern the pattern of exit status on which to take this transition * @return a builder to enable fluent chaining */ @@ -220,10 +225,8 @@ public class FlowBuilder { /** * A synonym for {@link #build()} which callers might find useful. Subclasses can override build to create an object * of the desired type (e.g. a parent builder or an actual flow). - * + * * @return the result of the builder - * - * @param Q the type of the result */ public final Q end() { return build(); @@ -325,10 +328,8 @@ public class FlowBuilder { for (String to : copy.keySet()) { if (!froms.contains(to)) { currentState = copy.get(to); - if (currentState != completedState) { + if (!currentState.isEndState()) { addTransition("COMPLETED", completedState); - } - if (currentState != failedState) { addTransition("*", failedState); } } @@ -337,12 +338,10 @@ public class FlowBuilder { // Then find the states that do not have a default transition for (String from : copy.keySet()) { currentState = copy.get(from); - if (currentState != failedState) { + if (!currentState.isEndState()) { if (!hasFail(from)) { addTransition("*", failedState); } - } - if (currentState != completedState) { if (!hasCompleted(from)) { addTransition("*", completedState); } @@ -373,23 +372,42 @@ public class FlowBuilder { if (transitions.size() == 1) { transitions.add(StateTransition.createEndStateTransition(failedState)); transitions.add(StateTransition.createEndStateTransition(completedState)); + transitions.add(StateTransition.createEndStateTransition(stoppedState)); + } + if (next.isEndState()) { + transitions.add(StateTransition.createEndStateTransition(next)); } dirty = true; } + private void stop(String pattern) { + addTransition(pattern, stoppedState); + } + + private void stop(String pattern, State restart) { + EndState next = new EndState(FlowExecutionStatus.STOPPED, "STOPPED", prefix + "stop" + (endCounter++), true); + addTransition(pattern, next); + currentState = next; + addTransition("*", restart); + } + private void end(String pattern) { addTransition(pattern, completedState); } + private void end(String pattern, String code) { + addTransition(pattern, new EndState(FlowExecutionStatus.COMPLETED, code, prefix + "end" + (endCounter++))); + } + private void fail(String pattern) { addTransition(pattern, failedState); } /** * A builder for continuing a flow from a decision state. - * + * * @author Dave Syer - * + * * @param the result of the builder's build() */ public static class UnterminatedFlowBuilder { @@ -404,9 +422,9 @@ public class FlowBuilder { * Start a transition to a new state if the exit status from the previous state matches the pattern given. * Successful completion normally results in an exit status equal to (or starting with by convention) * "COMPLETED". See {@link ExitStatus} for commonly used values. - * + * * @param pattern the pattern of exit status on which to take this transition - * @return + * @return a TransitionBuilder */ public TransitionBuilder on(String pattern) { return new TransitionBuilder(parent, pattern); @@ -416,9 +434,9 @@ public class FlowBuilder { /** * A builder for transitions within a flow. - * + * * @author Dave Syer - * + * * @param the result of the parent builder's build() */ public static class TransitionBuilder { @@ -434,7 +452,7 @@ public class FlowBuilder { /** * Specify the next step. - * + * * @param step the next step after this transition * @return a FlowBuilder */ @@ -447,8 +465,8 @@ public class FlowBuilder { /** * Specify the next state as a complete flow. - * - * @param step the next step after this transition + * + * @param flow the next flow after this transition * @return a FlowBuilder */ public FlowBuilder to(Flow flow) { @@ -460,8 +478,8 @@ public class FlowBuilder { /** * Specify the next state as a decision. - * - * @param step the next step after this transition + * + * @param decider the decider to determine the next step * @return a FlowBuilder */ public FlowBuilder to(JobExecutionDecider decider) { @@ -473,7 +491,53 @@ public class FlowBuilder { /** * Signal the successful end of the flow. - * + * + * @return a FlowBuilder + */ + public FlowBuilder stop() { + parent.stop(pattern); + return parent; + } + + /** + * Stop the flow and provide a flow to start with if the flow is restarted. + * + * @param flow the flow to restart with + * @return a FlowBuilder + */ + public FlowBuilder stopAndRestart(Flow flow) { + State next = parent.createState(flow); + parent.stop(pattern, next); + return parent; + } + + /** + * Stop the flow and provide a decider to start with if the flow is restarted. + * + * @param decider a decider to restart with + * @return a FlowBuilder + */ + public FlowBuilder stopAndRestart(JobExecutionDecider decider) { + State next = parent.createState(decider); + parent.stop(pattern, next); + return parent; + } + + /** + * Stop the flow and provide a step to start with if the flow is restarted. + * + * @param restart the step to restart with + * @return a FlowBuilder + */ + public FlowBuilder stopAndRestart(Step restart) { + State next = parent.createState(restart); + parent.stop(pattern, next); + return parent; + } + + /** + * Signal the successful end of the flow. + * * @return a FlowBuilder */ public FlowBuilder end() { @@ -481,9 +545,19 @@ public class FlowBuilder { return parent; } + /** + * Signal the end of the flow with the status provided. + * + * @return a FlowBuilder + */ + public FlowBuilder end(String status) { + parent.end(pattern, status); + return parent; + } + /** * Signal the end of the flow with an error condition. - * + * * @return a FlowBuilder */ public FlowBuilder fail() { @@ -494,22 +568,22 @@ public class FlowBuilder { /** * A builder for building a split state. Example (builder is a {@link FlowBuilder}): - * + * *
 	 * Flow splitFlow = builder.start(flow1).split(new SyncTaskExecutor()).add(flow2).build();
 	 * 
- * + * * where flow1 and flow2 will be executed (one after the other because of the task * executor that was added). Another example - * + * *
 	 * Flow splitFlow = builder.start(step1).split(new SimpleAsyncTaskExecutor()).add(flow).build();
 	 * 
- * + * * In this example, a flow consisting of step1 will be executed in parallel with flow. - * + * * @author Dave Syer - * + * * @param the result of the parent builder's build() */ public static class SplitBuilder { @@ -529,7 +603,7 @@ public class FlowBuilder { /** * Add flows to the split, in addition to the current state already present in the parent builder. - * + * * @param flows more flows to add to the split * @return the parent builder */ diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/job/builder/FlowJobBuilderTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/job/builder/FlowJobBuilderTests.java index 59b9a4481..7462d1095 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/job/builder/FlowJobBuilderTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/job/builder/FlowJobBuilderTests.java @@ -21,6 +21,7 @@ import org.junit.Before; import org.junit.Test; 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.JobInterruptedException; import org.springframework.batch.core.JobParameters; @@ -36,12 +37,12 @@ import org.springframework.core.task.SimpleAsyncTaskExecutor; /** * @author Dave Syer - * + * */ public class FlowJobBuilderTests { private JobRepository jobRepository; - + private JobExecution execution; private StepSupport step1 = new StepSupport("step1") { @@ -54,6 +55,16 @@ public class FlowJobBuilderTests { } }; + private StepSupport fails = new StepSupport("fails") { + @Override + public void execute(StepExecution stepExecution) throws JobInterruptedException, + UnexpectedJobExecutionException { + stepExecution.upgradeStatus(BatchStatus.FAILED); + stepExecution.setExitStatus(ExitStatus.FAILED); + jobRepository.update(stepExecution); + } + }; + private StepSupport step2 = new StepSupport("step2") { @Override public void execute(StepExecution stepExecution) throws JobInterruptedException, @@ -164,4 +175,41 @@ public class FlowJobBuilderTests { assertEquals(2, execution.getStepExecutions().size()); } + @Test + public void testBuildWithCustomEndState() throws Exception { + SimpleJobBuilder builder = new JobBuilder("flow").repository(jobRepository).start(step1); + builder.on("COMPLETED").end("FOO"); + builder.preventRestart(); + builder.build().execute(execution); + assertEquals(BatchStatus.COMPLETED, execution.getStatus()); + assertEquals("FOO", execution.getExitStatus().getExitCode()); + assertEquals(1, execution.getStepExecutions().size()); + } + + @Test + public void testBuildWithStop() throws Exception { + SimpleJobBuilder builder = new JobBuilder("flow").repository(jobRepository).start(step1); + builder.on("COMPLETED").stop(); + builder.preventRestart(); + builder.build().execute(execution); + assertEquals(BatchStatus.STOPPED, execution.getStatus()); + assertEquals("STOPPED", execution.getExitStatus().getExitCode()); + assertEquals(1, execution.getStepExecutions().size()); + } + + @Test + public void testBuildWithStopAndRestart() throws Exception { + SimpleJobBuilder builder = new JobBuilder("flow").repository(jobRepository).start(fails); + builder.on("FAILED").stopAndRestart(step2); + Job job = builder.build(); + job.execute(execution); + assertEquals(BatchStatus.STOPPED, execution.getStatus()); + assertEquals(1, execution.getStepExecutions().size()); + execution = jobRepository.createJobExecution("flow", new JobParameters()); + job.execute(execution); + assertEquals(BatchStatus.COMPLETED, execution.getStatus()); + assertEquals(1, execution.getStepExecutions().size()); + assertEquals("step2", execution.getStepExecutions().iterator().next().getStepName()); + } + }