diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/JobExecution.java b/spring-batch-core/src/main/java/org/springframework/batch/core/JobExecution.java index 1277e1908..e689b9b29 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/JobExecution.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/JobExecution.java @@ -21,8 +21,10 @@ import java.io.ObjectInputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Date; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.repeat.ExitStatus; @@ -273,12 +275,12 @@ public class JobExecution extends Entity { */ public List getAllFailureExceptions(){ - List allExceptions = new ArrayList(failureExceptions); + Set allExceptions = new HashSet(failureExceptions); for(StepExecution stepExecution: stepExecutions){ allExceptions.addAll(stepExecution.getFailureExceptions()); } - return allExceptions; + return new ArrayList(allExceptions); } /** 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 851a511cb..db05b3256 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 @@ -52,7 +52,7 @@ import org.springframework.util.ClassUtils; */ public abstract class AbstractJob implements Job, BeanNameAware, InitializingBean { - private static final Log logger = LogFactory.getLog(AbstractJob.class); + protected static final Log logger = LogFactory.getLog(AbstractJob.class); private String name; @@ -239,11 +239,13 @@ public abstract class AbstractJob implements Job, BeanNameAware, InitializingBea } catch (JobInterruptedException e) { logger.error(e); + execution.setExitStatus(ExitStatus.FAILED); execution.setStatus(BatchStatus.STOPPED); execution.addFailureException(e); } catch (Throwable t) { logger.error(t); + execution.setExitStatus(ExitStatus.FAILED); execution.setStatus(BatchStatus.FAILED); execution.addFailureException(t); } @@ -271,14 +273,18 @@ public abstract class AbstractJob implements Job, BeanNameAware, InitializingBea /** * Convenience method for subclasses to delegate the handling of a specific - * step in the context of the current {@link JobExecution}. + * step in the context of the current {@link JobExecution}. Clients of this + * method do not need access to the {@link JobRepository}, nor do they need + * to worry about populating the execution context on a restart, nor + * detecting the interrupted state (in job or step execution). * * @param step the {@link Step} to execute - * @param execution the currect {@link JobExecution} + * @param execution the current {@link JobExecution} * @return the {@link StepExecution} corresponding to this step * * @throws JobInterruptedException if the {@link JobExecution} has been - * interrupted + * interrupted, and in particular if {@link BatchStatus#STOPPED} or + * {@link BatchStatus#STOPPING} is detected * @throws StartLimitExceededException if the start limit has been exceeded * for this step * @throws JobRestartException if the job is in an inconsistent state from @@ -286,7 +292,7 @@ public abstract class AbstractJob implements Job, BeanNameAware, InitializingBea */ protected final StepExecution handleStep(Step step, JobExecution execution) throws JobInterruptedException, JobRestartException, StartLimitExceededException { - if (execution.getStatus() == BatchStatus.STOPPING) { + if (execution.getStatus() == BatchStatus.STOPPING || execution.getStatus() == BatchStatus.STOPPED) { throw new JobInterruptedException("JobExecution interrupted."); } @@ -313,6 +319,11 @@ public abstract class AbstractJob implements Job, BeanNameAware, InitializingBea step.execute(currentStepExecution); + if (currentStepExecution.getStatus() == BatchStatus.STOPPED + || currentStepExecution.getStatus() == BatchStatus.STOPPING) { + throw new JobInterruptedException("Job interrupted by step execution"); + } + } return currentStepExecution; diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/ConditionalJob.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/ConditionalJob.java index e57910f1c..c097a9aa1 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/job/ConditionalJob.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/ConditionalJob.java @@ -15,23 +15,230 @@ */ package org.springframework.batch.core.job; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.springframework.batch.core.Job; import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.JobExecutionException; +import org.springframework.batch.core.Step; import org.springframework.batch.core.StepExecution; +import org.springframework.batch.repeat.ExitStatus; /** + * A {@link Job} that branches conditionally depending on the exit status of the + * last step execution. The input parameters are the step transitions (in no + * particular order). The start step name must also be specified (and must exist + * in the set of transitions). + * * @author Dave Syer - * + * */ public class ConditionalJob extends AbstractJob { + private Step startStep; + + private Map> transitionMap = new HashMap>(); + + private Map stepMap = new HashMap(); + + private String startStepName; + + private Collection stepTransitions; + /** - * @see AbstractJob#doExecute(JobExecution) + * Create a {@link Job} with the given name. + * @param name + */ + public ConditionalJob(String name) { + super(name); + } + + /** + * Create a {@link Job} with no name. + */ + public ConditionalJob() { + super(); + } + + /** + * Public setter for the start step name. + * @param startStepName the name of the start step + */ + public void setStartStepName(String startStepName) { + this.startStepName = startStepName; + } + + /** + * Public setter for the stepTransitions. + * @param stepTransitions the stepTransitions to set + */ + public void setStepTransitions(Collection stepTransitions) { + + this.stepTransitions = stepTransitions; + } + + /** + * Locate start step and pre-populate data structures needed for execution. + * + * @see AbstractJob#afterPropertiesSet() */ @Override - protected StepExecution doExecute(JobExecution execution) throws JobExecutionException { - // TODO Auto-generated method stub - return null; + public void afterPropertiesSet() throws Exception { + + super.afterPropertiesSet(); + + startStep = null; + transitionMap.clear(); + stepMap.clear(); + boolean hasEndStep = false; + + for (StepTransition stepTransition : stepTransitions) { + Step step = stepTransition.getStep(); + stepMap.put(step.getName(), step); + } + + for (StepTransition stepTransition : stepTransitions) { + + Step step = stepTransition.getStep(); + + if (!stepTransition.isEnd()) { + + String next = stepTransition.getNext(); + + if (!stepMap.containsKey(next)) { + throw new IllegalArgumentException("Missing step for [" + stepTransition + "]"); + } + + } + else { + hasEndStep = true; + } + + String name = step.getName(); + + SortedSet set = transitionMap.get(name); + if (set == null) { + set = new TreeSet(); + transitionMap.put(name, set); + } + set.add(stepTransition); + + } + + if (!hasEndStep) { + throw new IllegalArgumentException( + "No end step was found. You must specify at least one transition with no next step."); + } + + if (startStepName != null) { + + startStep = stepMap.get(startStepName); + if (startStep == null) { + throw new IllegalArgumentException( + "Start step does not exist (if you specify a startStepName make sure " + + "a step with that name is in one of the transitions): [" + startStepName + "]"); + } + + } + else { + + // Try and locate a transition with no incoming links + + Set nextStepNames = new HashSet(); + + for (StepTransition stepTransition : stepTransitions) { + nextStepNames.add(stepTransition.getNext()); + } + + for (StepTransition stepTransition : stepTransitions) { + Step step = stepTransition.getStep(); + if (!nextStepNames.contains(step.getName())) { + if (startStep != null && !startStep.getName().equals(step.getName())) { + throw new IllegalArgumentException(String.format( + "Multiple possible start steps found: [%s, %s]. " + + "Please specify one explicitly with the startStepName property.", startStep + .getName(), step.getName())); + } + startStep = step; + } + } + + if (startStep == null) { + throw new IllegalArgumentException( + "No start step could be located (no transition without incoming links)"); + } + + } + + } + + /** + * @see AbstractJob#doExecute(JobExecution) + * @throws JobExecutionException if the next step cannot be located at any + * point + */ + @Override + protected StepExecution doExecute(JobExecution jobExecution) throws JobExecutionException { + StepExecution stepExecution = null; + Step step = nextStep(null); + while (step != null) { + stepExecution = handleStep(step, jobExecution); + step = nextStep(stepExecution); + } + return stepExecution; + } + + /** + * @param stepExecution the last {@link StepExecution} (or null if this is + * the start) + * @return the next {@link Step} (or null if this is the end) + * @throws JobExecutionException + */ + private Step nextStep(StepExecution stepExecution) throws JobExecutionException { + + if (stepExecution == null) { + return startStep; + } + + String stepName = stepExecution.getStepName(); + Set set = transitionMap.get(stepName); + + if (set == null) { + throw new JobExecutionException(String.format("No transitions found in job=%s for step=%s", getName(), + stepName)); + } + + ExitStatus status = stepExecution.getExitStatus(); + String next = null; + for (StepTransition stepTransition : set) { + if (stepTransition.matches(status)) { + if (stepTransition.isEnd()) { + // End of job + return null; + } + next = stepTransition.getNext(); + break; + } + } + + if (next == null) { + throw new JobExecutionException(String.format( + "Next step not found in job=%s for step=%s with exit status=%s", getName(), stepName, status)); + } + + if (!stepMap.containsKey(next)) { + throw new JobExecutionException(String.format("Next step not specified in job=%s for next=%s", getName(), + next)); + } + + return stepMap.get(next); + } } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/SimpleJob.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/SimpleJob.java index 4bcb4e235..0abc5b2bf 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/job/SimpleJob.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/SimpleJob.java @@ -76,7 +76,7 @@ public class SimpleJob extends AbstractJob { StepExecution stepExecution = null; for (Step step : steps) { stepExecution = handleStep(step, execution); - if (stepExecution.getStatus() == BatchStatus.FAILED || stepExecution.getStatus() == BatchStatus.STOPPED) { + if (stepExecution.getStatus() != BatchStatus.COMPLETED) { return stepExecution; } } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/StepTransition.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/StepTransition.java new file mode 100644 index 000000000..4b08373b5 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/StepTransition.java @@ -0,0 +1,284 @@ +/* + * 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; + +import org.springframework.batch.core.Step; +import org.springframework.batch.repeat.ExitStatus; +import org.springframework.util.StringUtils; + +/** + * Value object representing a potential transition from one {@link Step} to + * another. The originating step name and the next {@link Step} to execute are + * linked by a pattern for the {@link ExitStatus#getExitCode() exit code} of an + * execution of the originating step. + * + * @author Dave Syer + * + */ +public class StepTransition implements Comparable { + + private final String pattern; + + private final String next; + + private final Step step; + + /** + * Create a new end state {@link StepTransition} specification. This + * transition explicitly goes to an end state (i.e. no more executions). + * + * @see StepTransition#StepTransition(Step, String, String) + */ + public StepTransition(Step step, String pattern) { + this(step, pattern, null); + } + + /** + * Create a new {@link StepTransition} specification from one step to + * another (by name). + * + * @param step the step to be executed + * @param pattern the pattern to match in the {@link ExitStatus} of the step + * @param next the name of the next step to execute + */ + public StepTransition(Step step, String pattern, String next) { + super(); + this.step = step; + if (!StringUtils.hasText(pattern)) { + this.pattern = "*"; + } + else { + this.pattern = pattern; + } + this.next = next; + } + + /** + * Public getter for the next step name. + * @return the next + */ + public String getNext() { + return next; + } + + /** + * Public getter for the step. + * @return the step + */ + public Step getStep() { + return step; + } + + /** + * Check if the provided {@link ExitStatus} matches the pattern, signalling + * that the next step should be executed. + * + * @param status the {@link ExitStatus} to compare + * @return true if the pattern matches this status + */ + public boolean matches(ExitStatus status) { + return matchStrings(pattern, status.getExitCode()); + } + + /** + * Check for a special next step signalling the end of a job. + * + * @return true if this transition goes nowhere (there is no next) + */ + public boolean isEnd() { + return next == null; + } + + /** + * Lifted from AntPathMatcher in Spring Core. Tests whether or not a string + * matches against a pattern. The pattern may contain two special + * characters:
+ * '*' means zero or more characters
+ * '?' means one and only one character + * @param pattern pattern to match against. Must not be null. + * @param str string which must be matched against the pattern. Must not be + * null. + * @return true if the string matches against the pattern, or + * false otherwise. + */ + private boolean matchStrings(String pattern, String str) { + char[] patArr = pattern.toCharArray(); + char[] strArr = str.toCharArray(); + int patIdxStart = 0; + int patIdxEnd = patArr.length - 1; + int strIdxStart = 0; + int strIdxEnd = strArr.length - 1; + char ch; + + boolean containsStar = pattern.contains("*"); + + if (!containsStar) { + // No '*'s, so we make a shortcut + if (patIdxEnd != strIdxEnd) { + return false; // Pattern and string do not have the same size + } + for (int i = 0; i <= patIdxEnd; i++) { + ch = patArr[i]; + if (ch != '?') { + if (ch != strArr[i]) { + return false;// Character mismatch + } + } + } + return true; // String matches against pattern + } + + if (patIdxEnd == 0) { + return true; // Pattern contains only '*', which matches anything + } + + // Process characters before first star + while ((ch = patArr[patIdxStart]) != '*' && strIdxStart <= strIdxEnd) { + if (ch != '?') { + if (ch != strArr[strIdxStart]) { + return false;// Character mismatch + } + } + patIdxStart++; + strIdxStart++; + } + if (strIdxStart > strIdxEnd) { + // All characters in the string are used. Check if only '*'s are + // left in the pattern. If so, we succeeded. Otherwise failure. + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (patArr[i] != '*') { + return false; + } + } + return true; + } + + // Process characters after last star + while ((ch = patArr[patIdxEnd]) != '*' && strIdxStart <= strIdxEnd) { + if (ch != '?') { + if (ch != strArr[strIdxEnd]) { + return false;// Character mismatch + } + } + patIdxEnd--; + strIdxEnd--; + } + if (strIdxStart > strIdxEnd) { + // All characters in the string are used. Check if only '*'s are + // left in the pattern. If so, we succeeded. Otherwise failure. + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (patArr[i] != '*') { + return false; + } + } + return true; + } + + // process pattern between stars. padIdxStart and patIdxEnd point + // always to a '*'. + while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) { + int patIdxTmp = -1; + for (int i = patIdxStart + 1; i <= patIdxEnd; i++) { + if (patArr[i] == '*') { + patIdxTmp = i; + break; + } + } + if (patIdxTmp == patIdxStart + 1) { + // Two stars next to each other, skip the first one. + patIdxStart++; + continue; + } + // Find the pattern between padIdxStart & padIdxTmp in str between + // strIdxStart & strIdxEnd + int patLength = (patIdxTmp - patIdxStart - 1); + int strLength = (strIdxEnd - strIdxStart + 1); + int foundIdx = -1; + strLoop: for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + ch = patArr[patIdxStart + j + 1]; + if (ch != '?') { + if (ch != strArr[strIdxStart + i + j]) { + continue strLoop; + } + } + } + + foundIdx = strIdxStart + i; + break; + } + + if (foundIdx == -1) { + return false; + } + + patIdxStart = patIdxTmp; + strIdxStart = foundIdx + patLength; + } + + // All characters in the string are used. Check if only '*'s are left + // in the pattern. If so, we succeeded. Otherwise failure. + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (patArr[i] != '*') { + return false; + } + } + + return true; + } + + /** + * Sorts by decreasing specificity of pattern, based on just counting + * wildcards (with * taking precedence over ?). If wodlcard counts are equal + * then falls back to alphabetic comparison. Hence * > foo* > ??? > + * fo? > foo. + * @see Comparable#compareTo(Object) + */ + public int compareTo(StepTransition other) { + String value = other.pattern; + if (pattern.equals(value)) { + return 0; + } + int patternCount = StringUtils.countOccurrencesOf(pattern, "*"); + int valueCount = StringUtils.countOccurrencesOf(value, "*"); + if (patternCount > valueCount) { + return 1; + } + if (patternCount < valueCount) { + return -1; + } + patternCount = StringUtils.countOccurrencesOf(pattern, "?"); + valueCount = StringUtils.countOccurrencesOf(value, "?"); + if (patternCount > valueCount) { + return 1; + } + if (patternCount < valueCount) { + return -1; + } + return pattern.compareTo(value); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("StepTransition: step=%s, pattern=%s, next=%s", step.getName(), pattern, next); + } + +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/MapJobRepositoryFactoryBean.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/MapJobRepositoryFactoryBean.java index ceebfd091..a6c9593e9 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/MapJobRepositoryFactoryBean.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/MapJobRepositoryFactoryBean.java @@ -19,6 +19,17 @@ import org.springframework.beans.factory.FactoryBean; */ public class MapJobRepositoryFactoryBean extends AbstractJobRepositoryFactoryBean { + /** + * Convenience method to clear all the map daos globally, removing all + * entities. + */ + public static void clear() { + MapJobInstanceDao.clear(); + MapJobExecutionDao.clear(); + MapStepExecutionDao.clear(); + MapExecutionContextDao.clear(); + } + @Override protected JobExecutionDao createJobExecutionDao() throws Exception { return new MapJobExecutionDao(); diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/job/ConditionalJobTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/job/ConditionalJobTests.java new file mode 100644 index 000000000..486c3a0bb --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/job/ConditionalJobTests.java @@ -0,0 +1,221 @@ +/* + * 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobExecutionException; +import org.springframework.batch.core.JobInterruptedException; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.UnexpectedJobExecutionException; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean; +import org.springframework.batch.core.step.StepSupport; +import org.springframework.batch.repeat.ExitStatus; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; + +/** + * @author Dave Syer + * + */ +public class ConditionalJobTests { + + private ConditionalJob job = new ConditionalJob("job"); + + private JobExecution jobExecution; + + @Before + public void setUp() throws Exception { + MapJobRepositoryFactoryBean.clear(); + MapJobRepositoryFactoryBean factory = new MapJobRepositoryFactoryBean(); + factory.setTransactionManager(new ResourcelessTransactionManager()); + factory.afterPropertiesSet(); + JobRepository jobRepository = (JobRepository) factory.getObject(); + job.setJobRepository(jobRepository); + jobExecution = jobRepository.createJobExecution("job", new JobParameters()); + } + + @Test(expected = IllegalArgumentException.class) + public void testEmptySteps() throws Exception { + job.setStepTransitions(Collections. emptySet()); + job.afterPropertiesSet(); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoNextStepSpecified() throws Exception { + job.setStepTransitions(Collections.singleton(new StepTransition(new StepSupport("step"), "*", "foo"))); + job.afterPropertiesSet(); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoStartStep() throws Exception { + job.setStepTransitions(Arrays.asList(new StepTransition(new StepSupport("step"), "FAILED", "step"), + new StepTransition(new StepSupport("step"), "*"))); + job.afterPropertiesSet(); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoEndStep() throws Exception { + job.setStepTransitions(Collections.singleton(new StepTransition(new StepSupport("step"), "FAILED", "step"))); + job.setStartStepName("step"); + job.afterPropertiesSet(); + } + + @Test(expected = IllegalArgumentException.class) + public void testMultipleStartSteps() throws Exception { + job.setStepTransitions(Arrays.asList(new StepTransition(new StubStep("step1"), "*"), new StepTransition( + new StubStep("step2"), "*"))); + job.afterPropertiesSet(); + } + + @Test + public void testNoMatchForNextStep() throws Exception { + job.setStepTransitions(Arrays.asList(new StepTransition(new StubStep("step1"), "FOO", "step2"), + new StepTransition(new StubStep("step2"), "*"))); + job.afterPropertiesSet(); + try { + job.doExecute(jobExecution); + fail("Expected JobExecutionException"); + } + catch (JobExecutionException e) { + // expected + String message = e.getMessage(); + assertTrue("Wrong message: " + message, message.toLowerCase().contains("next step not found")); + } + } + + @Test + public void testOneStep() throws Exception { + job.setStepTransitions(Arrays.asList(new StepTransition(new StubStep("step1"), "*"))); + job.afterPropertiesSet(); + StepExecution stepExecution = job.doExecute(jobExecution); + assertEquals(ExitStatus.FINISHED, stepExecution.getExitStatus()); + assertEquals(1, jobExecution.getStepExecutions().size()); + } + + @Test + public void testExplicitStartStep() throws Exception { + job.setStepTransitions(Arrays.asList(new StepTransition(new StubStep("step"), "FAILED", "step"), + new StepTransition(new StubStep("step"), "*"))); + job.setStartStepName("step"); + job.afterPropertiesSet(); + StepExecution stepExecution = job.doExecute(jobExecution); + assertEquals(ExitStatus.FINISHED, stepExecution.getExitStatus()); + assertEquals(1, jobExecution.getStepExecutions().size()); + } + + @Test + public void testTwoSteps() throws Exception { + job.setStepTransitions(Arrays.asList(new StepTransition(new StubStep("step1"), "*", "step2"), + new StepTransition(new StubStep("step2"), "*"))); + job.afterPropertiesSet(); + StepExecution stepExecution = job.doExecute(jobExecution); + assertEquals(ExitStatus.FINISHED, stepExecution.getExitStatus()); + assertEquals(2, jobExecution.getStepExecutions().size()); + } + + @Test + public void testFailedStep() throws Exception { + job.setStepTransitions(Arrays.asList(new StepTransition(new StubStep("step1") { + @Override + public void execute(StepExecution stepExecution) throws JobInterruptedException, + UnexpectedJobExecutionException { + stepExecution.setStatus(BatchStatus.FAILED); + stepExecution.setExitStatus(ExitStatus.FAILED); + } + }, "*", "step2"), new StepTransition(new StubStep("step2"), "*"))); + job.afterPropertiesSet(); + StepExecution stepExecution = job.doExecute(jobExecution); + assertEquals(ExitStatus.FINISHED, stepExecution.getExitStatus()); + assertEquals(BatchStatus.COMPLETED, stepExecution.getStatus()); + assertEquals(2, jobExecution.getStepExecutions().size()); + } + + @Test + public void testStoppingStep() throws Exception { + job.setStepTransitions(Arrays.asList(new StepTransition(new StubStep("step1") { + @Override + public void execute(StepExecution stepExecution) throws JobInterruptedException, + UnexpectedJobExecutionException { + stepExecution.setStatus(BatchStatus.STOPPED); + } + }, "*", "step2"), + new StepTransition(new StubStep("step2"), "*"))); + job.afterPropertiesSet(); + try { + job.doExecute(jobExecution); + fail("Expected JobInterruptedException"); + } catch (JobInterruptedException e) { + // expected + } + assertEquals(1, jobExecution.getStepExecutions().size()); + } + + @Test + public void testBranching() throws Exception { + job.setStepTransitions(Arrays.asList(new StepTransition(new StubStep("step1"), "*", "step2"), + new StepTransition(new StubStep("step1"), "COMPLETED", "step3"), new StepTransition(new StubStep( + "step2"), "*"), new StepTransition(new StubStep("step3"), "*"))); + job.afterPropertiesSet(); + StepExecution stepExecution = job.doExecute(jobExecution); + assertEquals(ExitStatus.FINISHED, stepExecution.getExitStatus()); + assertEquals(2, jobExecution.getStepExecutions().size()); + assertEquals("step3", stepExecution.getStepName()); + } + + /** + * @author Dave Syer + * + */ + private static class StubStep extends StepSupport { + + /** + * + */ + public StubStep() { + super(); + } + + /** + * @param string + */ + public StubStep(String string) { + super(string); + } + + /** + * @see StepSupport#execute(StepExecution) + */ + @Override + public void execute(StepExecution stepExecution) throws JobInterruptedException, + UnexpectedJobExecutionException { + stepExecution.setStatus(BatchStatus.COMPLETED); + stepExecution.setExitStatus(ExitStatus.FINISHED); + } + + } + +} 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 ca400ead8..37ecab213 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 @@ -110,20 +110,18 @@ public class SimpleJobTests { job = new SimpleJob(); job.setJobRepository(jobRepository); - step1 = new StubStep("TestStep1"); + step1 = new StubStep("TestStep1", jobRepository); step1.setCallback(new Runnable() { public void run() { list.add("default"); } }); - step2 = new StubStep("TestStep2"); + step2 = new StubStep("TestStep2", jobRepository); step2.setCallback(new Runnable() { public void run() { list.add("default"); } }); - step1.setJobRepository(jobRepository); - step2.setJobRepository(jobRepository); List steps = new ArrayList(); steps.add(step1); @@ -250,8 +248,8 @@ public class SimpleJobTests { final JobInterruptedException exception = new JobInterruptedException("Interrupt!"); step1.setProcessException(exception); job.execute(jobExecution); - assertEquals(1, jobExecution.getAllFailureExceptions().size()); - assertEquals(exception, jobExecution.getAllFailureExceptions().get(0)); + assertEquals(2, jobExecution.getAllFailureExceptions().size()); + assertEquals(exception, jobExecution.getStepExecutions().iterator().next().getFailureExceptions().get(0)); assertEquals(0, list.size()); checkRepository(BatchStatus.STOPPED, ExitStatus.FAILED); } @@ -418,22 +416,24 @@ public class SimpleJobTests { @Test public void testInterruptJob() throws Exception { - step1 = new StubStep("interruptStep") { + step1 = new StubStep("interruptStep", jobRepository) { public void execute(StepExecution stepExecution) throws JobInterruptedException, UnexpectedJobExecutionException { stepExecution.getJobExecution().stop(); + super.execute(stepExecution); } }; job.setSteps(Arrays.asList(new Step[] { step1, step2 })); job.execute(jobExecution); - Throwable expected = jobExecution.getAllFailureExceptions().get(0); - assertTrue(expected instanceof JobInterruptedException); - assertEquals("JobExecution interrupted.", expected.getMessage()); + assertEquals(1, jobExecution.getAllFailureExceptions().size()); + Throwable expected = jobExecution.getAllFailureExceptions().get(0); + assertTrue("Wrong exception "+expected, expected instanceof JobInterruptedException); + assertEquals("JobExecution interrupted.", expected.getMessage()); - assertNull("Second step was not executed", step2.passedInStepContext); + assertNull("Second step was not supposed to be executed", step2.passedInStepContext); } /* @@ -446,7 +446,7 @@ public class SimpleJobTests { assertEquals(jobInstance.getId(), jobExecution.getJobId()); assertEquals(status, jobExecution.getStatus()); if (exitStatus != null) { - assertEquals(jobExecution.getExitStatus().getExitCode(), exitStatus.getExitCode()); + assertEquals(exitStatus.getExitCode(), jobExecution.getExitStatus().getExitCode()); } } @@ -469,8 +469,9 @@ public class SimpleJobTests { /** * @param string */ - public StubStep(String string) { + public StubStep(String string, JobRepository jobRepository) { super(string); + this.jobRepository = jobRepository; } /** @@ -529,14 +530,5 @@ public class SimpleJobTests { } - /** - * Public setter for {@link JobRepository}. - * - * @param jobRepository is a mandatory dependence (no default). - */ - public void setJobRepository(JobRepository jobRepository) { - this.jobRepository = jobRepository; - } - } } diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/job/StepTransitionTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/job/StepTransitionTests.java new file mode 100644 index 000000000..98eb2e7a1 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/job/StepTransitionTests.java @@ -0,0 +1,129 @@ +/* + * 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; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.batch.core.step.StepSupport; +import org.springframework.batch.repeat.ExitStatus; + +/** + * @author Dave Syer + * + */ +public class StepTransitionTests { + + @Test + public void testIsEnd() { + StepTransition transition = new StepTransition(new StepSupport(), ""); + assertTrue(transition.isEnd()); + assertNull(transition.getNext()); + } + + @Test + public void testMatchesStar() { + StepTransition transition = new StepTransition(new StepSupport(), "*", "start"); + assertTrue(transition.matches(ExitStatus.CONTINUABLE)); + } + + @Test + public void testMatchesNull() { + StepTransition transition = new StepTransition(new StepSupport(), null, "start"); + assertTrue(transition.matches(ExitStatus.CONTINUABLE)); + } + + @Test + public void testMatchesEmpty() { + StepTransition transition = new StepTransition(new StepSupport(), "", "start"); + assertTrue(transition.matches(ExitStatus.CONTINUABLE)); + } + + @Test + public void testMatchesExact() { + StepTransition transition = new StepTransition(new StepSupport(), "CONTINUABLE", "start"); + assertTrue(transition.matches(ExitStatus.CONTINUABLE)); + } + + @Test + public void testMatchesWildcard() { + StepTransition transition = new StepTransition(new StepSupport(), "CONTIN*", "start" ); + assertTrue(transition.matches(ExitStatus.CONTINUABLE)); + } + + @Test + public void testMatchesPlaceholder() { + StepTransition transition = new StepTransition(new StepSupport(), "CONTIN???LE", "start"); + assertTrue(transition.matches(ExitStatus.CONTINUABLE)); + } + + @Test + public void testSimpleOrderingEqual() { + StepTransition transition = new StepTransition(new StepSupport(), "CONTIN???LE", "start"); + assertEquals(0, transition.compareTo(transition)); + } + + @Test + public void testSimpleOrderingMoreGeneral() { + StepTransition transition = new StepTransition(new StepSupport(), "CONTIN???LE", "start"); + StepTransition other = new StepTransition(new StepSupport(), "CONTINUABLE", "start"); + assertEquals(1, transition.compareTo(other)); + assertEquals(-1, other.compareTo(transition)); + } + + @Test + public void testSimpleOrderingMostGeneral() { + StepTransition transition = new StepTransition(new StepSupport(), "*", "start"); + StepTransition other = new StepTransition(new StepSupport(), "CONTINUABLE", "start"); + assertEquals(1, transition.compareTo(other)); + assertEquals(-1, other.compareTo(transition)); + } + + @Test + public void testSubstringAndWildcard() { + StepTransition transition = new StepTransition(new StepSupport(), "CONTIN*", "start"); + StepTransition other = new StepTransition(new StepSupport(), "CONTINUABLE", "start"); + assertEquals(1, transition.compareTo(other)); + assertEquals(-1, other.compareTo(transition)); + } + + @Test + public void testSimpleOrderingMostToNextGeneral() { + StepTransition transition = new StepTransition(new StepSupport(), "*", "start"); + StepTransition other = new StepTransition(new StepSupport(), "C?", "start"); + assertEquals(1, transition.compareTo(other)); + assertEquals(-1, other.compareTo(transition)); + } + + @Test + public void testSimpleOrderingAdjacent() { + StepTransition transition = new StepTransition(new StepSupport(), "CON*", "start"); + StepTransition other = new StepTransition(new StepSupport(), "CON?", "start"); + assertEquals(1, transition.compareTo(other)); + assertEquals(-1, other.compareTo(transition)); + } + + @Test + public void testToString() { + StepTransition transition = new StepTransition(new StepSupport(), "CONTIN???LE", "start"); + String string = transition.toString(); + assertTrue("Wrong string: " + string, string.contains("StepTransition")); + assertTrue("Wrong string: " + string, string.contains("start")); + assertTrue("Wrong string: " + string, string.contains("CONTIN???LE")); + assertTrue("Wrong string: " + string, string.contains("next=")); + } + +}