From 881c18cccdb6e3ff8ab351c7a937cc1979674c55 Mon Sep 17 00:00:00 2001 From: lucasward Date: Thu, 24 Jan 2008 16:17:42 +0000 Subject: [PATCH] BATCH-304: Modified the CommandLineJobRunner to accept a job name as an argument. Also modified the contract of JobParamterFactory to return an empty JobParameters or Properties if null is passed in, rather than throwing an IllegalArgumentException. --- .../support/CommandLineJobRunner.java | 184 +++++++++++++----- .../support/DefaultJobParametersFactory.java | 9 +- .../bootstrap/support/ExitCodeMapper.java | 1 - .../ScheduledJobParametersFactory.java | 9 +- .../support/CommandLineJobRunnerTests.java | 89 +++++++-- .../DefaultJobParametersFactoryTests.java | 5 + .../ScheduledJobParametersFactoryTests.java | 5 + .../bootstrap/support/test-environment.xml | 7 +- 8 files changed, 240 insertions(+), 69 deletions(-) diff --git a/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/CommandLineJobRunner.java b/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/CommandLineJobRunner.java index 4c50955ad..a0cdde6ea 100644 --- a/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/CommandLineJobRunner.java +++ b/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/CommandLineJobRunner.java @@ -15,28 +15,104 @@ */ package org.springframework.batch.execution.bootstrap.support; +import java.util.Properties; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.batch.core.domain.IncorrectJobCountException; import org.springframework.batch.core.domain.Job; import org.springframework.batch.core.domain.JobExecution; +import org.springframework.batch.core.domain.JobLocator; import org.springframework.batch.core.domain.JobParameters; -import org.springframework.batch.core.domain.NoSuchJobException; import org.springframework.batch.core.executor.ExitCodeExceptionClassifier; -import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; import org.springframework.batch.core.runtime.JobParametersFactory; import org.springframework.batch.execution.launch.JobLauncher; import org.springframework.batch.execution.step.simple.SimpleExitCodeExceptionClassifier; -import org.springframework.batch.repeat.ExitStatus; -import org.springframework.beans.factory.access.SingletonBeanFactoryLocator; +import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.util.StringUtils; /** - * @author Lucas Ward + *

+ * Basic launcher for starting jobs from the command line. In general, it is + * assumed that this launcher will primarily be used to start a job via a script + * from an Enterprise Scheduler. Therefore, exit codes are mapped to integers so + * that schedulers can use the returned values to determine the next course of + * action. The returned values can also be useful to operations teams in + * determining what should happen upon failure. For example, a returned code of + * 5 might mean that some resource wasn't available and the job should be + * restarted. However, a code of 10 might mean that something critical has + * happened and the issue should be escalated. + *

* + *

+ * With any launch of a batch job within Spring Batch, a Spring context + * containing the Job and the 'Execution Environment' has to be created. This + * command line launcher can be used to load that context from a single + * location. It can also load the job as well All dependencies of the launcher + * will then be satisfied by autowiring by type from the combined application + * context. Default values are provided for all fields except the + * {@link JobLauncher} and {@link JobLocator}. Therefore, if autowiring fails + * to set it (it should be noted that dependency checking is disabled because + * most of the fields have default values and thus don't require dependencies to + * be fulfilled via autowiring) then an exception will be thrown. It should also + * be noted that even if an exception is thrown by this class, it will be mapped + * to an integer and returned. + *

+ * + *

+ * Notice a property is available to set the {@link SystemExiter}. This class + * is used to exit from the main method, rather than calling System.exit() + * directly. This is because unit testing a class the calls System.exit() is + * impossible without kicking off the test within a new Jvm, which it is + * possible to do, however it is a complex solution, much more so than + * strategizing the exiter. + *

+ * + *

+ * The arguments to this class are roughly as follows: + *

+ * + * + * java jobPath jobName jobLauncherPath jobParameters... + * + * + *

+ *

+ *

+ * + *

+ * The combined application context must only contain one instance of a + * {@link JobLauncher}. The job parameters passed in to the command line will + * be converted to {@link Properties} by assuming that each individual element + * is one parameter that is separated by an equals sign. For example, + * "vendor.id=290232". Below is an example arguments list: " + * + *

+ * + * java org.springframework.batch.execution.bootstrap.support.CommandLineJobRunner testJob.xml + * testJob standard-job-launcher.xml schedule.date=2008/01/24 vendor.id=3902483920 + *

+ * + *

Once arguments have been successfully parsed, autowiring will be used to set + * various dependencies. The {@JobLauncher} for example, will be loaded this way. If + * none is contained in the bean factory (it searches by type) then a + * {@link BeanDefinitionStoreException} will be thrown. The same exception will also + * be thrown if there is more than one present. Assuming the JobLauncher has been + * set correctly, the jobName argument will be used to obtain an actual {@link Job}. + * If a {@link JobLocator} has been set, then it will be used, if not the beanFactory + * will be asked, using the jobName as the bean id.

+ * + * @author Dave Syer + * @author Lucas Ward + * @since 1.0 */ public class CommandLineJobRunner { @@ -49,9 +125,11 @@ public class CommandLineJobRunner { private JobLauncher launcher; + private JobLocator jobLocator; + private SystemExiter systemExiter = new JvmSystemExiter(); - private JobParametersFactory jobParametersFactory = new ScheduledJobParametersFactory(); + private JobParametersFactory jobParametersFactory = new DefaultJobParametersFactory(); /** * Injection setter for the {@link JobLauncher}. @@ -101,17 +179,34 @@ public class CommandLineJobRunner { systemExiter.exit(status); } - int start(String jobPath, String environmentPath, String[] parameters) { + public void setJobLocator(JobLocator jobLocator) { + this.jobLocator = jobLocator; + } + + /* + * Start a job by obtaining a combined classpath using the job launcher and + * job paths. If a JobLocator has been set, then use it to obtain an actual + * job, if not ask the context for it. + */ + int start(String jobPath, String jobLauncherPath, String jobName, + String[] parameters) { try { - ApplicationContext context = new ClassPathXmlApplicationContext(new String[] { - jobPath, environmentPath }); - context.getAutowireCapableBeanFactory().autowireBeanProperties(this, - AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, false); - Job job = getJob(context); + ApplicationContext context = new ClassPathXmlApplicationContext( + new String[] { jobPath, jobLauncherPath }); + context.getAutowireCapableBeanFactory().autowireBeanProperties( + this, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, false); - JobParameters jobParameters = jobParametersFactory.getJobParameters(StringUtils - .splitArrayElementsIntoProperties(parameters, "=")); + Job job; + if (jobLocator != null) { + job = jobLocator.getJob(jobName); + } else { + job = (Job) context.getBean(jobName); + } + + JobParameters jobParameters = jobParametersFactory + .getJobParameters(StringUtils + .splitArrayElementsIntoProperties(parameters, "=")); JobExecution jobExecution = launcher.run(job, jobParameters); return exitCodeMapper.getExitCode(jobExecution.getExitStatus() @@ -123,25 +218,8 @@ public class CommandLineJobRunner { } } - private Job getJob(ApplicationContext context) - throws IncorrectJobCountException, NoSuchJobException { - - String[] jobs = context.getBeanNamesForType(Job.class); - - if (jobs.length > 1) { - logger.error("More than one job exists in the provided context: [" + "jobPath" + "]"); - throw new IncorrectJobCountException( - "More than one job exists in the provided context. Bean Names: [" - + jobs + "]"); - } else if (jobs.length == 0) { - throw new NoSuchJobException("No jobs found in the provided context."); - } - - return (Job) context.getBean(jobs[0]); - } - /** - * Launch a batch job using a {@link SimpleCommandLineJobRunner}. Creates a + * Launch a batch job using a {@link CommandLineJobRunner}. Creates a * new Spring context for the job execution, and uses a common parent for * all such contexts. No exception are thrown from this method, rather * exceptions are logged and an integer returned through the exit status in @@ -149,30 +227,36 @@ public class CommandLineJobRunner { * Spring context). * * @param args + *

*

    - *
  • -Djob.configuration.path: the classpath location of the - * JobConfiguration to use - *
  • -Djob.name: job name to be passed to the + *
  • jobPath: the xml application context containing a + * {@link Job} + *
  • jobName: the bean id of the job. + *
  • jobLauncherPath: the xml application context containing a * {@link JobLauncher} - *
  • -Dbatch.execution.environment.key: the key in - * beanRefContext.xml used to load the execution environment - * which will be the parent context for the job execution - * (mandatory if -Dbean.ref.context is specified). - *
  • -Dbean.ref.context: the location for beanRefContext.xml - * (optional, default is to only use the context specified in the - * job.configuration.path) (@see - * {@link SingletonBeanFactoryLocator}).
  • + *
  • jobParameters: 0 to many parameters that will be used to + * launch a job. *
+ *

*/ public static void main(String[] args) { - String jobPath = args[0]; - String executionPath = args[1]; - String[] parameters = new String[args.length - 2]; - System.arraycopy(args, 2, parameters, 0, args.length - 2); - CommandLineJobRunner command = new CommandLineJobRunner(); - int result = command.start(jobPath, executionPath, parameters); + + if (args.length < 3) { + logger + .error("At least 3 arguments are required: JobPath, JobName, and ExecutionPath."); + command.exit(1); + } + + String jobPath = args[0]; + String jobName = args[1]; + String jobLauncherPath = args[2]; + String[] parameters = new String[args.length - 3]; + System.arraycopy(args, 2, parameters, 0, args.length - 3); + + int result = command.start(jobPath, jobLauncherPath, jobName, + parameters); command.exit(result); } diff --git a/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/DefaultJobParametersFactory.java b/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/DefaultJobParametersFactory.java index bf283cf44..6b76919cd 100644 --- a/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/DefaultJobParametersFactory.java +++ b/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/DefaultJobParametersFactory.java @@ -72,7 +72,9 @@ public class DefaultJobParametersFactory implements JobParametersFactory { */ public JobParameters getJobParameters(Properties props) { - Assert.notNull(props, "Factory arguments must not be null."); + if(props == null || props.isEmpty()){ + return new JobParameters(); + } JobParametersBuilder propertiesBuilder = new JobParametersBuilder(); @@ -123,6 +125,11 @@ public class DefaultJobParametersFactory implements JobParametersFactory { * @see org.springframework.batch.core.runtime.JobParametersFactory#getProperties(org.springframework.batch.core.domain.JobParameters) */ public Properties getProperties(JobParameters params) { + + if(params == null || params.isEmpty()){ + return new Properties(); + } + Map parameters = params.getParameters(); Properties result = new Properties(); for (Iterator iterator = parameters.keySet().iterator(); iterator.hasNext();) { diff --git a/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/ExitCodeMapper.java b/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/ExitCodeMapper.java index 41afe4af3..41d74e7bd 100644 --- a/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/ExitCodeMapper.java +++ b/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/ExitCodeMapper.java @@ -18,7 +18,6 @@ public interface ExitCodeMapper { static int JVM_EXITCODE_JOB_ERROR = 2; public static final String NO_SUCH_JOB = "NO_SUCH_JOB"; public static final String JOB_NOT_PROVIDED = "JOB_NOT_PROVIDED"; - public static final String MULTIPLE_JOBS_FOUND = "MULTIPLE_JOBS_FOUND"; /** * Transform the exitcode known by the batchframework into an exitcode in the diff --git a/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/ScheduledJobParametersFactory.java b/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/ScheduledJobParametersFactory.java index 9cdd89094..04748ac17 100644 --- a/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/ScheduledJobParametersFactory.java +++ b/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/ScheduledJobParametersFactory.java @@ -45,7 +45,9 @@ public class ScheduledJobParametersFactory implements JobParametersFactory { */ public JobParameters getJobParameters(Properties props) { - Assert.notNull(props, "Factory arguments must not be null."); + if(props == null || props.isEmpty()){ + return new JobParameters(); + } JobParametersBuilder propertiesBuilder = new JobParametersBuilder(); @@ -77,6 +79,11 @@ public class ScheduledJobParametersFactory implements JobParametersFactory { * @see org.springframework.batch.core.runtime.JobParametersFactory#getProperties(org.springframework.batch.core.domain.JobParameters) */ public Properties getProperties(JobParameters params) { + + if(params == null || params.isEmpty()){ + return new Properties(); + } + Map parameters = params.getParameters(); Properties result = new Properties(); for (Iterator iterator = parameters.keySet().iterator(); iterator.hasNext();) { diff --git a/spring-batch-execution/src/test/java/org/springframework/batch/execution/bootstrap/support/CommandLineJobRunnerTests.java b/spring-batch-execution/src/test/java/org/springframework/batch/execution/bootstrap/support/CommandLineJobRunnerTests.java index 3de4fd17a..63df8eb3c 100644 --- a/spring-batch-execution/src/test/java/org/springframework/batch/execution/bootstrap/support/CommandLineJobRunnerTests.java +++ b/spring-batch-execution/src/test/java/org/springframework/batch/execution/bootstrap/support/CommandLineJobRunnerTests.java @@ -15,15 +15,16 @@ */ package org.springframework.batch.execution.bootstrap.support; +import junit.framework.TestCase; + import org.springframework.batch.core.domain.Job; import org.springframework.batch.core.domain.JobExecution; import org.springframework.batch.core.domain.JobParameters; +import org.springframework.batch.core.executor.ExitCodeExceptionClassifier; import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; import org.springframework.batch.execution.launch.JobLauncher; import org.springframework.batch.repeat.ExitStatus; -import junit.framework.TestCase; - /** * @author Lucas Ward * @@ -32,27 +33,32 @@ public class CommandLineJobRunnerTests extends TestCase { private static final String JOB = "org/springframework/batch/execution/bootstrap/support/job.xml"; private static final String TEST_BATCH_ENVIRONMENT = "org/springframework/batch/execution/bootstrap/support/test-environment.xml"; + private static final String JOB_NAME = "test-job"; + + private String jobPath = JOB; + private String environmentPath = TEST_BATCH_ENVIRONMENT; + private String jobName = JOB_NAME; + private String jobKey = "job.Key=myKey"; + private String scheduleDate = "schedule.Date=01/23/2008"; + private String vendorId = "vendor.id=33243243"; + + private String[] args = new String[]{jobPath, jobName, environmentPath, jobKey, scheduleDate, vendorId}; + + private JobExecution jobExecution; /* (non-Javadoc) * @see junit.framework.TestCase#setUp() */ protected void setUp() throws Exception { super.setUp(); + + jobExecution = new JobExecution(null, new Long(1)); + ExitStatus exitStatus = ExitStatus.FINISHED; + jobExecution.setExitStatus(exitStatus); } public void testMain(){ - String jobPath = JOB; - String environmentPath = TEST_BATCH_ENVIRONMENT; - String jobKey = "job.Key=myKey"; - String scheduleDate = "schedule.Date=01/23/2008"; - String vendorId = "vendor.id=33243243"; - - String[] args = new String[]{jobPath, environmentPath, jobKey, scheduleDate, vendorId}; - - JobExecution jobExecution = new JobExecution(null, new Long(1)); - ExitStatus exitStatus = ExitStatus.FINISHED; - jobExecution.setExitStatus(exitStatus); StubJobLauncher.jobExecution = jobExecution; CommandLineJobRunner.main(args); @@ -60,6 +66,35 @@ public class CommandLineJobRunnerTests extends TestCase { assertEquals(0, StubSystemExiter.getStatus()); } + public void testJobAlreadyRunning(){ + + StubJobLauncher.throwExecutionRunningException = true; + + CommandLineJobRunner.main(args); + + assertTrue(StubExceptionClassifier.exception instanceof JobExecutionAlreadyRunningException); + } + + //can't test because it will cause the system to exit. +// public void testInvalidArgs(){ +// +// String[] args = new String[]{jobPath, jobName}; +// CommandLineJobRunner.main(args); +// } + + public void testWithNoParameters(){ + + String[] args = new String[]{jobPath, jobName, environmentPath}; + CommandLineJobRunner.main(args); + assertEquals(new JobParameters(), StubJobLauncher.jobParameters); + } + + protected void tearDown() throws Exception { + super.tearDown(); + + StubJobLauncher.tearDown(); + } + public static class StubSystemExiter implements SystemExiter { public static int status; @@ -77,15 +112,43 @@ public class CommandLineJobRunnerTests extends TestCase { public static JobExecution jobExecution; public static boolean throwExecutionRunningException = false; + public static JobParameters jobParameters; public JobExecution run(Job job, JobParameters jobParameters) throws JobExecutionAlreadyRunningException { + StubJobLauncher.jobParameters = jobParameters; + if(throwExecutionRunningException){ throw new JobExecutionAlreadyRunningException(""); } return jobExecution; } + + public static void tearDown(){ + jobExecution = null; + throwExecutionRunningException = false; + jobParameters = null; + } + } + + public static class StubExceptionClassifier implements ExitCodeExceptionClassifier{ + + public static Throwable exception; + + public Object classify(Throwable throwable) { + return null; + } + + public Object getDefault() { + return null; + } + + public ExitStatus classifyForExitCode(Throwable throwable) { + exception = throwable; + return ExitStatus.FAILED; + } + } } diff --git a/spring-batch-execution/src/test/java/org/springframework/batch/execution/bootstrap/support/DefaultJobParametersFactoryTests.java b/spring-batch-execution/src/test/java/org/springframework/batch/execution/bootstrap/support/DefaultJobParametersFactoryTests.java index 81721ba47..c082a3c85 100644 --- a/spring-batch-execution/src/test/java/org/springframework/batch/execution/bootstrap/support/DefaultJobParametersFactoryTests.java +++ b/spring-batch-execution/src/test/java/org/springframework/batch/execution/bootstrap/support/DefaultJobParametersFactoryTests.java @@ -69,4 +69,9 @@ public class DefaultJobParametersFactoryTests extends TestCase { JobParameters props = factory.getJobParameters(new Properties()); assertTrue(props.getParameters().isEmpty()); } + + public void testNullArgs(){ + assertEquals(new JobParameters(), factory.getJobParameters(null)); + assertEquals(new Properties(), factory.getProperties(null)); + } } diff --git a/spring-batch-execution/src/test/java/org/springframework/batch/execution/bootstrap/support/ScheduledJobParametersFactoryTests.java b/spring-batch-execution/src/test/java/org/springframework/batch/execution/bootstrap/support/ScheduledJobParametersFactoryTests.java index 6e139d63f..50338ea23 100644 --- a/spring-batch-execution/src/test/java/org/springframework/batch/execution/bootstrap/support/ScheduledJobParametersFactoryTests.java +++ b/spring-batch-execution/src/test/java/org/springframework/batch/execution/bootstrap/support/ScheduledJobParametersFactoryTests.java @@ -75,4 +75,9 @@ public class ScheduledJobParametersFactoryTests extends TestCase { JobParameters props = factory.getJobParameters(new Properties()); assertTrue(props.getParameters().isEmpty()); } + + public void testNullArgs(){ + assertEquals(new JobParameters(), factory.getJobParameters(null)); + assertEquals(new Properties(), factory.getProperties(null)); + } } diff --git a/spring-batch-execution/src/test/resources/org/springframework/batch/execution/bootstrap/support/test-environment.xml b/spring-batch-execution/src/test/resources/org/springframework/batch/execution/bootstrap/support/test-environment.xml index 57f320612..e8dc1e376 100644 --- a/spring-batch-execution/src/test/resources/org/springframework/batch/execution/bootstrap/support/test-environment.xml +++ b/spring-batch-execution/src/test/resources/org/springframework/batch/execution/bootstrap/support/test-environment.xml @@ -8,12 +8,13 @@ http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd"> - - + class="org.springframework.batch.execution.bootstrap.support.CommandLineJobRunnerTests$StubSystemExiter" /> + +