diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/Job.java b/spring-batch-core/src/main/java/org/springframework/batch/core/Job.java index 6e576eeda..583ed2911 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/Job.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/Job.java @@ -34,7 +34,7 @@ public interface Job { * @return true if this job can be restarted after a failure */ boolean isRestartable(); - + /** * Run the {@link JobExecution} and update the meta information like status * and statistics as necessary. This method should not throw any exceptions @@ -54,4 +54,6 @@ public interface Job { */ JobParametersIncrementer getJobParametersIncrementer(); + void validate(JobParameters parameters) throws JobParametersInvalidException; + } \ No newline at end of file diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/JobParametersInvalidException.java b/spring-batch-core/src/main/java/org/springframework/batch/core/JobParametersInvalidException.java new file mode 100644 index 000000000..cbb0eab7e --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/JobParametersInvalidException.java @@ -0,0 +1,16 @@ +package org.springframework.batch.core; + +/** + * Exception for {@link Job} to signal that some {@link JobParameters} are + * invalid. + * + * @author Dave Syer + * + */ +public class JobParametersInvalidException extends JobExecutionException { + + public JobParametersInvalidException(String msg) { + super(msg); + } + +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/GroupAwareJob.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/GroupAwareJob.java index e664247fd..c96c0829c 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/GroupAwareJob.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/GroupAwareJob.java @@ -17,7 +17,9 @@ package org.springframework.batch.core.configuration.support; import org.springframework.batch.core.Job; import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.JobParametersIncrementer; +import org.springframework.batch.core.JobParametersInvalidException; /** * A {@link Job} that can optionally prepend a group name to another job's name, @@ -68,6 +70,10 @@ public class GroupAwareJob implements Job { public void execute(JobExecution execution) { delegate.execute(execution); } + + public void validate(JobParameters parameters) throws JobParametersInvalidException { + delegate.validate(parameters); + } /** * Concatenates the group name and the delegate job name (joining with a 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 565a62376..8d9fec2da 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 @@ -76,6 +76,11 @@ public class JobParser extends AbstractSingleBeanDefinitionParser { builder.addPropertyReference("jobRepository", repositoryAttribute); } + String parametersValidator = element.getAttribute("parameters-validator"); + if (StringUtils.hasText(parametersValidator)) { + builder.addPropertyReference("jobParametersValidator", parametersValidator); + } + String restartableAttribute = element.getAttribute("restartable"); if (StringUtils.hasText(restartableAttribute)) { builder.addPropertyValue("restartable", restartableAttribute); diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/JobParserJobFactoryBean.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/JobParserJobFactoryBean.java index d54caecbe..43b5ebbd9 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/JobParserJobFactoryBean.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/JobParserJobFactoryBean.java @@ -17,6 +17,7 @@ package org.springframework.batch.core.configuration.xml; import org.springframework.batch.core.JobExecutionListener; import org.springframework.batch.core.JobParametersIncrementer; +import org.springframework.batch.core.job.JobParametersValidator; import org.springframework.batch.core.job.flow.Flow; import org.springframework.batch.core.job.flow.FlowJob; import org.springframework.batch.core.repository.JobRepository; @@ -42,6 +43,8 @@ class JobParserJobFactoryBean implements SmartFactoryBean { private JobRepository jobRepository; + private JobParametersValidator jobParametersValidator; + private JobExecutionListener[] jobExecutionListeners; private JobParametersIncrementer jobParametersIncrementer; @@ -64,6 +67,10 @@ class JobParserJobFactoryBean implements SmartFactoryBean { flowJob.setJobRepository(jobRepository); } + if (jobParametersValidator != null) { + flowJob.setJobParametersValidator(jobParametersValidator); + } + if (jobExecutionListeners != null) { flowJob.setJobExecutionListeners(jobExecutionListeners); } @@ -87,6 +94,10 @@ class JobParserJobFactoryBean implements SmartFactoryBean { public void setJobRepository(JobRepository jobRepository) { this.jobRepository = jobRepository; } + + public void setJobParametersValidator(JobParametersValidator jobParametersValidator) { + this.jobParametersValidator = jobParametersValidator; + } public JobRepository getJobRepository() { return this.jobRepository; 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 0b3628b05..5f04c6b91 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 @@ -29,7 +29,9 @@ import org.springframework.batch.core.JobExecutionException; import org.springframework.batch.core.JobExecutionListener; import org.springframework.batch.core.JobInstance; import org.springframework.batch.core.JobInterruptedException; +import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.JobParametersIncrementer; +import org.springframework.batch.core.JobParametersInvalidException; import org.springframework.batch.core.StartLimitExceededException; import org.springframework.batch.core.Step; import org.springframework.batch.core.StepExecution; @@ -45,10 +47,10 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** - * Abstract implementation of the {@link Job} interface. Common dependencies such as a - * {@link JobRepository}, {@link JobExecutionListener}s, and various configuration - * parameters are set here. Therefore, common error handling and listener calling - * activities are abstracted away from implementations. + * Abstract implementation of the {@link Job} interface. Common dependencies + * such as a {@link JobRepository}, {@link JobExecutionListener}s, and various + * configuration parameters are set here. Therefore, common error handling and + * listener calling activities are abstracted away from implementations. * * @author Lucas Ward * @author Dave Syer @@ -67,6 +69,8 @@ public abstract class AbstractJob implements Job, StepLocator, BeanNameAware, In private JobParametersIncrementer jobParametersIncrementer; + private JobParametersValidator jobParametersValidator = new DefaultJobParametersValidator(); + /** * Default constructor. */ @@ -85,6 +89,26 @@ public abstract class AbstractJob implements Job, StepLocator, BeanNameAware, In this.name = name; } + /** + * A validator for job parameters. Defaults to a vanilla + * {@link DefaultJobParametersValidator}. + * + * @param jobParametersValidator a validator instance + */ + public void setJobParametersValidator(JobParametersValidator jobParametersValidator) { + this.jobParametersValidator = jobParametersValidator; + } + + /** + * Delegates to the {@link #setJobParametersValidator validator} supplied + * (defaults to just checking for null parameters). + * + * @see Job#validate(JobParameters) + */ + public void validate(JobParameters parameters) throws JobParametersInvalidException { + jobParametersValidator.validate(parameters); + } + /** * Assert mandatory properties: {@link JobRepository}. * @@ -129,21 +153,21 @@ public abstract class AbstractJob implements Job, StepLocator, BeanNameAware, In } /** - * Retrieve the step with the given name. If there is no Step with the - * given name, then return null. + * Retrieve the step with the given name. If there is no Step with the given + * name, then return null. * * @param stepName * @return the Step */ public abstract Step getStep(String stepName); - + /** * Retrieve the step names. * * @return the step names */ public abstract Collection getStepNames(); - + /** * Boolean flag to prevent categorically a job from restarting, even if it * has failed previously. @@ -235,7 +259,7 @@ public abstract class AbstractJob implements Job, StepLocator, BeanNameAware, In */ public final void execute(JobExecution execution) { - logger.debug("Job execution starting: "+execution); + logger.debug("Job execution starting: " + execution); try { @@ -248,8 +272,9 @@ public abstract class AbstractJob implements Job, StepLocator, BeanNameAware, In try { doExecute(execution); - logger.debug("Job execution complete: "+execution); - } catch (RepeatException e) { + logger.debug("Job execution complete: " + execution); + } + catch (RepeatException e) { throw e.getCause(); } } @@ -259,7 +284,7 @@ public abstract class AbstractJob implements Job, StepLocator, BeanNameAware, In // with it in the same way as any other interruption. execution.setStatus(BatchStatus.STOPPED); execution.setExitStatus(ExitStatus.COMPLETED); - logger.debug("Job execution was stopped: "+execution); + logger.debug("Job execution was stopped: " + execution); } @@ -291,9 +316,9 @@ public abstract class AbstractJob implements Job, StepLocator, BeanNameAware, In catch (Exception e) { logger.error("Exception encountered in afterStep callback", e); } - + jobRepository.update(execution); - + } } @@ -347,9 +372,10 @@ public abstract class AbstractJob implements Job, StepLocator, BeanNameAware, In logger.info("Executing step: [" + step + "]"); try { step.execute(currentStepExecution); - } catch (JobInterruptedException e) { + } + catch (JobInterruptedException e) { // Ensure that the job gets the message that it is stopping - // and can pass it on to other steps that are executing + // and can pass it on to other steps that are executing // concurrently. execution.setStatus(BatchStatus.STOPPING); throw e; @@ -357,13 +383,15 @@ public abstract class AbstractJob implements Job, StepLocator, BeanNameAware, In jobRepository.updateExecutionContext(execution); - if (currentStepExecution.getStatus() == BatchStatus.STOPPING || currentStepExecution.getStatus() == BatchStatus.STOPPED) { + if (currentStepExecution.getStatus() == BatchStatus.STOPPING + || currentStepExecution.getStatus() == BatchStatus.STOPPED) { // Ensure that the job gets the message that it is stopping execution.setStatus(BatchStatus.STOPPING); throw new JobInterruptedException("Job interrupted by step execution"); } - } else { + } + else { // currentStepExecution.setExitStatus(ExitStatus.NOOP); } @@ -411,11 +439,11 @@ public abstract class AbstractJob implements Job, StepLocator, BeanNameAware, In + "so it may be dangerous to proceed. " + "Manual intervention is probably necessary."); } - if ((stepStatus == BatchStatus.COMPLETED && step.isAllowStartIfComplete() == false) + if ((stepStatus == BatchStatus.COMPLETED && step.isAllowStartIfComplete() == false) || stepStatus == BatchStatus.ABANDONED) { // step is complete, false should be returned, indicating that the // step should not be started - logger.info("Step already complete or not restartable, so no action to execute: "+lastStepExecution); + logger.info("Step already complete or not restartable, so no action to execute: " + lastStepExecution); return false; } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/DefaultJobParametersValidator.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/DefaultJobParametersValidator.java new file mode 100644 index 000000000..f6015cb9b --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/DefaultJobParametersValidator.java @@ -0,0 +1,75 @@ +package org.springframework.batch.core.job; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; + +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersInvalidException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link JobParametersValidator}. + * + * @author Dave Syer + * + */ +public class DefaultJobParametersValidator implements JobParametersValidator, InitializingBean { + + private Collection requiredKeys = new HashSet(); + + private Collection optionalKeys = new HashSet(); + + /** + * Check that there are no overlaps between required and optional keys. + * @throws IllegalStateException if there is an overlap + */ + public void afterPropertiesSet() throws IllegalStateException { + for (String key : requiredKeys) { + Assert.state(!optionalKeys.contains(key), "Optional keys canot be required: "+key); + } + } + + /** + * Check the parameters meet the specification provided. + * + * @see JobParametersValidator#validate(JobParameters) + */ + public void validate(JobParameters parameters) throws JobParametersInvalidException { + + if (parameters == null) { + throw new JobParametersInvalidException("The JobParameters can not be null"); + } + + Collection missingKeys = new HashSet(); + for (String key : requiredKeys) { + if (!parameters.getParameters().containsKey(key)) { + missingKeys.add(key); + } + } + if (!missingKeys.isEmpty()) { + throw new JobParametersInvalidException("The JobParameters do not contain required keys: " + missingKeys); + } + + } + + /** + * The keys that are required in the parameters. + * + * @param requiredKeys the required key values + */ + public void setRequiredKeys(String[] requiredKeys) { + this.requiredKeys = new HashSet(Arrays.asList(requiredKeys)); + } + + /** + * The keys that are optional in the parameters. + * + * @param optionalKeys the optional key values + */ + public void setOptionalKeys(String[] optionalKeys) { + this.optionalKeys = new HashSet(Arrays.asList(optionalKeys)); + } + +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/JobParametersValidator.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/JobParametersValidator.java new file mode 100644 index 000000000..eabce91a4 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/JobParametersValidator.java @@ -0,0 +1,24 @@ +package org.springframework.batch.core.job; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersInvalidException; + +/** + * Strategy interface for a {@link Job} to use in validating parameters. + * + * @author Dave Syer + * + */ +public interface JobParametersValidator { + + /** + * Check the parameters meet whatever requirements are appropriate, and + * throw an exception if not. + * + * @param parameters some {@link JobParameters} + * @throws JobParametersInvalidException if the parameters are invalid + */ + void validate(JobParameters parameters) throws JobParametersInvalidException; + +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/JobLauncher.java b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/JobLauncher.java index 90aa4c1ac..59fdd3cbd 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/JobLauncher.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/JobLauncher.java @@ -18,6 +18,7 @@ package org.springframework.batch.core.launch; import org.springframework.batch.core.Job; import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersInvalidException; import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; import org.springframework.batch.core.repository.JobRestartException; @@ -42,8 +43,8 @@ public interface JobLauncher { * always be returned by this method, regardless of whether or not the * execution was successful. If there is a past {@link JobExecution} which * has paused, the same {@link JobExecution} is returned instead of a new - * one created. A exception will only be thrown if there is a failure to - * start the job. If the job encounters some error while processing, the + * one created. A exception will only be thrown if there is a failure to + * start the job. If the job encounters some error while processing, the * JobExecution will be returned, and the status will need to be inspected. * * @return the {@link JobExecution} if it returns synchronously. If the @@ -57,8 +58,10 @@ public interface JobLauncher { * circumstances that preclude a re-start. * @throws JobInstanceAlreadyCompleteException if the job has been run * before with the same parameters and completed successfully + * @throws JobParametersInvalidException if the parameters are not valid for + * this job */ public JobExecution run(Job job, JobParameters jobParameters) throws JobExecutionAlreadyRunningException, - JobRestartException, JobInstanceAlreadyCompleteException; + JobRestartException, JobInstanceAlreadyCompleteException, JobParametersInvalidException; } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/JobOperator.java b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/JobOperator.java index c50cccef5..2c4427b5d 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/JobOperator.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/JobOperator.java @@ -24,6 +24,7 @@ import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.JobInstance; import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.JobParametersIncrementer; +import org.springframework.batch.core.JobParametersInvalidException; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.UnexpectedJobExecutionException; import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; @@ -31,10 +32,10 @@ import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteExcep import org.springframework.batch.core.repository.JobRestartException; /** - * Low level interface for inspecting and controlling jobs with access - * only to primitive and collection types. Suitable for a command-line client - * (e.g. that launches a new process for each operation), or a remote launcher - * like a JMX console. + * Low level interface for inspecting and controlling jobs with access only to + * primitive and collection types. Suitable for a command-line client (e.g. that + * launches a new process for each operation), or a remote launcher like a JMX + * console. * * @author Dave Syer * @since 2.0 @@ -98,8 +99,9 @@ public interface JobOperator { * name * @throws JobInstanceAlreadyExistsException if a job instance with this * name and parameters already exists + * @throws JobParametersInvalidException */ - Long start(String jobName, String parameters) throws NoSuchJobException, JobInstanceAlreadyExistsException; + Long start(String jobName, String parameters) throws NoSuchJobException, JobInstanceAlreadyExistsException, JobParametersInvalidException; /** * Restart a failed or stopped {@link JobExecution}. Fails with an exception @@ -117,9 +119,11 @@ public interface JobOperator { * corresponding {@link Job} is no longer available for launching * @throws JobRestartException if there is a non-specific error with the * restart (e.g. corrupt or inconsistent restart data) + * @throws JobParametersInvalidException if the parameters are not valid for + * this job */ Long restart(long executionId) throws JobInstanceAlreadyCompleteException, NoSuchJobExecutionException, - NoSuchJobException, JobRestartException; + NoSuchJobException, JobRestartException, JobParametersInvalidException; /** * Launch the next in a sequence of {@link JobInstance} determined by the @@ -138,10 +142,12 @@ public interface JobOperator { * is launched * @throws NoSuchJobException if there is no such job definition available * @throws JobParametersNotFoundException if the parameters cannot be found + * @throws JobParametersInvalidException + * @throws UnexpectedJobExecutionException * @throws UnexpectedJobExecutionException if an unexpected condition arises */ Long startNextInstance(String jobName) throws NoSuchJobException, JobParametersNotFoundException, - JobRestartException, JobExecutionAlreadyRunningException, JobInstanceAlreadyCompleteException; + JobRestartException, JobExecutionAlreadyRunningException, JobInstanceAlreadyCompleteException, UnexpectedJobExecutionException, JobParametersInvalidException; /** * Send a stop signal to the {@link JobExecution} with the supplied id. The diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/SimpleJobLauncher.java b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/SimpleJobLauncher.java index 78dd3b7b2..0871f37ca 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/SimpleJobLauncher.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/SimpleJobLauncher.java @@ -21,6 +21,7 @@ import org.springframework.batch.core.Job; import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.JobInstance; import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersInvalidException; import org.springframework.batch.core.launch.JobLauncher; import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; @@ -74,13 +75,18 @@ public class SimpleJobLauncher implements JobLauncher, InitializingBean { * re-start is either not allowed or not needed. * @throws JobInstanceAlreadyCompleteException if this instance has already * completed successfully + * @throws JobParametersInvalidException */ public JobExecution run(final Job job, final JobParameters jobParameters) - throws JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException { + throws JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException, + JobParametersInvalidException { Assert.notNull(job, "The Job must not be null."); Assert.notNull(jobParameters, "The JobParameters must not be null."); + // Allow the job to veto the execution + job.validate(jobParameters); + final JobExecution jobExecution; JobExecution lastExecution = jobRepository.getLastJobExecution(job.getName(), jobParameters); if (lastExecution != null) { @@ -112,7 +118,8 @@ public class SimpleJobLauncher implements JobLauncher, InitializingBean { + "] and the following status: [" + jobExecution.getStatus() + "]"); } catch (Throwable t) { - logger.info("Job: [" + job + "] failed unexpectedly and fatally with the following parameters: [" + jobParameters + "]", t); + logger.info("Job: [" + job + "] failed unexpectedly and fatally with the following parameters: [" + + jobParameters + "]", t); rethrow(t); } } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/SimpleJobOperator.java b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/SimpleJobOperator.java index a48670a9c..977c42431 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/SimpleJobOperator.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/SimpleJobOperator.java @@ -31,6 +31,7 @@ import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.JobInstance; import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.JobParametersIncrementer; +import org.springframework.batch.core.JobParametersInvalidException; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.UnexpectedJobExecutionException; import org.springframework.batch.core.configuration.JobRegistry; @@ -251,7 +252,7 @@ public class SimpleJobOperator implements JobOperator, InitializingBean { * org.springframework.batch.core.launch.JobOperator#resume(java.lang.Long) */ public Long restart(long executionId) throws JobInstanceAlreadyCompleteException, NoSuchJobExecutionException, - NoSuchJobException, JobRestartException { + NoSuchJobException, JobRestartException, JobParametersInvalidException { logger.info("Checking status of job execution with id=" + executionId); @@ -279,7 +280,7 @@ public class SimpleJobOperator implements JobOperator, InitializingBean { * org.springframework.batch.core.launch.JobOperator#start(java.lang.String, * java.lang.String) */ - public Long start(String jobName, String parameters) throws NoSuchJobException, JobInstanceAlreadyExistsException { + public Long start(String jobName, String parameters) throws NoSuchJobException, JobInstanceAlreadyExistsException, JobParametersInvalidException { logger.info("Checking status of job with name=" + jobName); @@ -319,7 +320,7 @@ public class SimpleJobOperator implements JobOperator, InitializingBean { * @see JobOperator#startNextInstance(String ) */ public Long startNextInstance(String jobName) throws NoSuchJobException, JobParametersNotFoundException, - UnexpectedJobExecutionException { + UnexpectedJobExecutionException, JobParametersInvalidException { logger.info("Locating parameters for next instance of job with name=" + jobName); 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 de722f3b1..3a50fd696 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 @@ -1,8 +1,9 @@ - @@ -21,13 +22,15 @@ Defines a job composed of a set of steps and transitions between steps. The job will be exposed in - the enclosing bean factory as a component of type Job - that can be launched using a JobLauncher. + the enclosing + bean factory as a component of type Job + that can be launched using a + JobLauncher. - + @@ -39,7 +42,8 @@ - + @@ -48,7 +52,22 @@ - + + + + + + + + + + + - + - + Defines a stage in job processing backed by a Step. The id attribute must be specified since this - step definition will be referred to from other elements + step definition + will be referred to from other elements to form a Job flow. - + - + @@ -100,7 +122,7 @@ - + @@ -110,7 +132,8 @@ A reference to a JobExecutionListener (or a POJO if using before-job-method / after-job-method or - source level annotations). + source level + annotations). @@ -125,7 +148,8 @@ - A bean definition for a step listener (or POJO if using *-method attributes or source level + A bean definition for a step listener (or POJO if + using *-method attributes or source level annotations) @@ -148,7 +172,8 @@ - + - + - + Defines a stage in job processing backed by a Step. The id attribute must be specified. The - step requires either a chunk definition, - a tasklet reference, or a reference to a + step requires either + a chunk definition, + a tasklet reference, or a reference to a (possibly abstract) parent step. - + - + - + - Declares job should split here into two or more subflows. + Declares job should split here into two or more + subflows. @@ -260,7 +289,8 @@ - A subflow within a job, having the same format as a job, but without a separate identity. + A subflow within a job, having the same + format as a job, but without a separate identity. @@ -270,14 +300,17 @@ - + - - + @@ -288,7 +321,8 @@ - Declares job should query a decider to determine where execution should go next. + Declares job should query a decider to determine + where execution should go next. @@ -297,13 +331,15 @@ - The decider is a reference to a JobExecutionDecider that can produce a status to base + The decider is a reference to a + JobExecutionDecider that can produce a status to base the next transition on. - + @@ -315,9 +351,12 @@ - - - + + + - + - + - The tasklet is a reference to another bean definition that implements the Tasklet interface. + The tasklet is a reference to another bean definition that implements + the Tasklet interface. - + @@ -351,7 +394,8 @@ ]]> - + - + - + - @@ -418,7 +465,8 @@ - @@ -444,8 +492,8 @@ - - + + @@ -459,8 +507,9 @@ - - + + @@ -471,8 +520,9 @@ - - + + @@ -483,11 +533,13 @@ - - + + - + - + @@ -520,7 +573,8 @@ - + @@ -530,7 +584,8 @@ - + - + - + - + - + - + - + - + - - + @@ -679,7 +743,7 @@ - + @@ -687,28 +751,30 @@ - + - Classify an exception as "excluded" from the set. + Classify an exception as "excluded" from the + set. - + - + A reference to a listener, a POJO with a listener-annotated method, or a POJO with - a method referenced by a *-method attribute. + a method referenced by a + *-method attribute. @@ -757,7 +823,8 @@ - + @@ -768,7 +835,8 @@ Defines a transition from this step to the - next one depending on the value of the exit + next + one depending on the value of the exit status. @@ -777,85 +845,107 @@ A pattern to match against the exit status code. Use * and ? as wildcard characters. When a - step finishes the most - specific match will be chosen to select the next step. Hint: + step finishes + the most + specific match will be chosen to select the next step. + Hint: always include a default - transition with on="*". - - + transition with on="*". + + + + - The name of the step to go to next. Must resolve to one of the other steps in this job. + The name of the step to go to next. Must + resolve to one of the other steps in this job. - + - Declares job should be stop at this point and provides pointer where execution should continue when - the job is restarted. + Declares job should be stop at this point and + provides pointer where execution should continue when + the job is + restarted. - + - A pattern to match against the exit status code. Use * and ? as wildcard characters. - When a step finishes the most specific match will be chosen to select the next step. - - - - The name of the step to start on when the stopped job is restarted. - Must resolve to one of the other steps in this job. + A pattern to match against the exit status + code. Use * and ? as wildcard characters. + When a step finishes the most specific match will be chosen to + select the next step. - - + + + The name of the step to start on when the + stopped job is restarted. + Must resolve to one of the other steps in this job. + + + + + - Declares job should end at this point, without the possibility of restart. - BatchStatus will be COMPLETED. ExitStatus is configurable. + Declares job should end at this point, without + the possibility of restart. + BatchStatus will be COMPLETED. ExitStatus is configurable. - + - A pattern to match against the exit status code. Use * and ? as wildcard characters. - When a step finishes the most specific match will be chosen to select the next step. + A pattern to match against the exit status + code. Use * and ? as wildcard characters. + When a step finishes the most specific match will be chosen to + select the next step. - + - The exit code value to end on, defaults to COMPLETED. + The exit code value to end on, defaults to + COMPLETED. - - + + - Declares job should fail at this point. BatchStatus will be FAILED. ExitStatus is configurable. + Declares job should fail at this point. + BatchStatus will be FAILED. ExitStatus is configurable. - + - A pattern to match against the exit status code. Use * and ? as wildcard characters. - When a step finishes the most specific match will be chosen to select the next step. + A pattern to match against the exit status + code. Use * and ? as wildcard characters. + When a step finishes the most specific match will be chosen to + select the next step. - + - The exit code value to end on, defaults to FAILED. + The exit code value to end on, defaults to + FAILED. - - + + - + @@ -879,10 +969,11 @@ - The name of the parent bean from which the configuration should inherit. + The name of the parent bean from which the + configuration should inherit. - + @@ -892,11 +983,15 @@ - Is this bean "abstract", that is, not meant to be instantiated itself - but rather just serving as parent for concrete child bean definitions? - The default is "false". Specify "true" to tell the bean factory to not - try to instantiate that particular bean in any case. - + Is this bean "abstract", that is, not meant to be + instantiated itself + but rather just serving as parent for concrete + child bean definitions? + The default is "false". Specify "true" to + tell the bean factory to not + try to instantiate that particular bean + in any case. + Note: This attribute will not be inherited by child bean definitions. Hence, it needs to be specified per abstract bean definition. @@ -908,24 +1003,27 @@ - Should this list be merged with the corresponding list provided - by the parent? If not, it will overwrite the parent list. + Should this list be merged with the corresponding + list provided + by the parent? If not, it will overwrite the parent list. - + - This attribute indicates the method from the class that should + This attribute indicates the method from the + class that should be used to dynamically create a proxy. - + - + diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/JobParserTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/JobParserTests.java index 81d3b6d67..97a08ce94 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/JobParserTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/JobParserTests.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import org.junit.BeforeClass; @@ -28,6 +29,7 @@ import org.springframework.aop.framework.Advised; import org.springframework.batch.core.Job; import org.springframework.batch.core.JobExecutionListener; import org.springframework.batch.core.job.AbstractJob; +import org.springframework.batch.core.job.DefaultJobParametersValidator; import org.springframework.batch.core.listener.JobExecutionListenerSupport; import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.repository.support.SimpleJobRepository; @@ -182,6 +184,18 @@ public class JobParserTests { } } + @Test + public void testParametersValidator() { + ApplicationContext ctx = jobParserParentAttributeTestsCtx; + Job job = (Job) ctx.getBean("jobWithParametersValidator"); + assertTrue(job instanceof AbstractJob); + Object validator = ReflectionTestUtils.getField(job, "jobParametersValidator"); + assertTrue(validator instanceof DefaultJobParametersValidator); + @SuppressWarnings("unchecked") + Collection keys = (Collection) ReflectionTestUtils.getField(validator, "requiredKeys"); + assertEquals(2, keys.size()); + } + @Test public void testListenerClearingJob() throws Exception { assertEquals(0, getListeners("listenerClearingJob", jobParserParentAttributeTestsCtx).size()); diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/job/DefaultJobParametersValidatorTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/job/DefaultJobParametersValidatorTests.java new file mode 100644 index 000000000..bb3488b2f --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/job/DefaultJobParametersValidatorTests.java @@ -0,0 +1,43 @@ +package org.springframework.batch.core.job; + +import org.junit.Test; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.JobParametersInvalidException; + +public class DefaultJobParametersValidatorTests { + + private DefaultJobParametersValidator validator = new DefaultJobParametersValidator(); + + @Test(expected = JobParametersInvalidException.class) + public void testValidateNull() throws Exception { + validator.validate(null); + } + + @Test + public void testValidateRequiredValues() throws Exception { + validator.setRequiredKeys(new String[] { "name", "value" }); + validator + .validate(new JobParametersBuilder().addString("name", "foo").addLong("value", 111L).toJobParameters()); + } + + @Test(expected = JobParametersInvalidException.class) + public void testValidateRequiredValuesMissing() throws Exception { + validator.setRequiredKeys(new String[] { "name", "value" }); + validator.validate(new JobParameters()); + } + + @Test + public void testValidateOptionalValues() throws Exception { + validator.setOptionalKeys(new String[] { "name", "value" }); + validator.validate(new JobParameters()); + } + + @Test(expected = IllegalStateException.class) + public void testOptionalValuesAlsoRequired() throws Exception { + validator.setOptionalKeys(new String[] { "name", "value" }); + validator.setRequiredKeys(new String[] { "foo", "value" }); + validator.afterPropertiesSet(); + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/job/AbstractJobTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/job/ExtendedAbstractJobTests.java similarity index 86% rename from spring-batch-core/src/test/java/org/springframework/batch/core/job/AbstractJobTests.java rename to spring-batch-core/src/test/java/org/springframework/batch/core/job/ExtendedAbstractJobTests.java index 99766586e..e78d4502b 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/job/AbstractJobTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/job/ExtendedAbstractJobTests.java @@ -31,6 +31,7 @@ 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.JobParametersInvalidException; import org.springframework.batch.core.Step; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.repository.JobRepository; @@ -42,7 +43,7 @@ import org.springframework.batch.support.transaction.ResourcelessTransactionMana * @author Dave Syer * */ -public class AbstractJobTests { +public class ExtendedAbstractJobTests { AbstractJob job = new StubJob("job"); @@ -109,6 +110,28 @@ public class AbstractJobTests { assertTrue(e.getMessage().contains("JobRepository")); } } + + @Test(expected=JobParametersInvalidException.class) + public void testValidatorWithNullParameters() throws Exception { + job.validate(null); + } + + @Test + public void testValidatorWithNotNullParameters() throws Exception { + job.validate(new JobParameters()); + // Should be free of side effects + } + + @Test(expected=JobParametersInvalidException.class) + public void testSetValidator() throws Exception { + job.setJobParametersValidator(new DefaultJobParametersValidator() { + @Override + public void validate(JobParameters parameters) throws JobParametersInvalidException { + throw new JobParametersInvalidException("Expected"); + } + }); + job.validate(new JobParameters()); + } /** * Runs the step and persists job execution context. diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/job/JobSupport.java b/spring-batch-core/src/test/java/org/springframework/batch/core/job/JobSupport.java index ec70d3aad..2e102a54c 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/job/JobSupport.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/job/JobSupport.java @@ -21,7 +21,9 @@ import java.util.List; import org.springframework.batch.core.Job; import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.JobParametersIncrementer; +import org.springframework.batch.core.JobParametersInvalidException; import org.springframework.batch.core.Step; import org.springframework.batch.core.UnexpectedJobExecutionException; import org.springframework.beans.factory.BeanNameAware; @@ -132,6 +134,13 @@ public class JobSupport implements BeanNameAware, Job { public boolean isRestartable() { return restartable; } + + /** + * @see Job#validate(JobParameters) + */ + public void validate(JobParameters parameters) throws JobParametersInvalidException { + + } /* * (non-Javadoc) diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantExceptionClassesTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantExceptionClassesTests.java index cd856f4ab..d1d816e95 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantExceptionClassesTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantExceptionClassesTests.java @@ -17,10 +17,7 @@ import org.springframework.batch.core.Step; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.job.SimpleJob; import org.springframework.batch.core.launch.JobLauncher; -import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; -import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.repository.JobRestartException; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -308,8 +305,7 @@ public class FaultTolerantExceptionClassesTests implements ApplicationContextAwa assertEquals("[1, 1, 1, 1]", tasklet.getCommitted().toString()); } - private StepExecution launchStep(String stepName) throws JobExecutionAlreadyRunningException, JobRestartException, - JobInstanceAlreadyCompleteException { + private StepExecution launchStep(String stepName) throws Exception { SimpleJob job = new SimpleJob(); job.setName("job"); job.setJobRepository(jobRepository); diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/JobParserParentAttributeTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/JobParserParentAttributeTests-context.xml index c1401b8ff..99e347737 100644 --- a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/JobParserParentAttributeTests-context.xml +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/JobParserParentAttributeTests-context.xml @@ -1,84 +1,117 @@ - - - + + + + - + - - + + + + - + - - + + + + - + - + + + - + + + - + + + - - + + + + - + + + + + + + + + + + - + - + - + - - - + + - - + + + + - +