1 | /* |
2 | * Copyright 2006-2008 the original author or authors. |
3 | * |
4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
5 | * you may not use this file except in compliance with the License. |
6 | * You may obtain a copy of the License at |
7 | * |
8 | * http://www.apache.org/licenses/LICENSE-2.0 |
9 | * |
10 | * Unless required by applicable law or agreed to in writing, software |
11 | * distributed under the License is distributed on an "AS IS" BASIS, |
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | * See the License for the specific language governing permissions and |
14 | * limitations under the License. |
15 | */ |
16 | package org.springframework.batch.core.launch.support; |
17 | |
18 | import java.util.ArrayList; |
19 | import java.util.Arrays; |
20 | import java.util.HashMap; |
21 | import java.util.HashSet; |
22 | import java.util.List; |
23 | import java.util.Map; |
24 | import java.util.Properties; |
25 | import java.util.Set; |
26 | |
27 | import org.apache.commons.logging.Log; |
28 | import org.apache.commons.logging.LogFactory; |
29 | import org.springframework.batch.core.BatchStatus; |
30 | import org.springframework.batch.core.ExitStatus; |
31 | import org.springframework.batch.core.Job; |
32 | import org.springframework.batch.core.JobExecution; |
33 | import org.springframework.batch.core.JobInstance; |
34 | import org.springframework.batch.core.JobParameter; |
35 | import org.springframework.batch.core.JobParameters; |
36 | import org.springframework.batch.core.JobParametersIncrementer; |
37 | import org.springframework.batch.core.configuration.JobLocator; |
38 | import org.springframework.batch.core.converter.DefaultJobParametersConverter; |
39 | import org.springframework.batch.core.converter.JobParametersConverter; |
40 | import org.springframework.batch.core.explore.JobExplorer; |
41 | import org.springframework.batch.core.launch.JobLauncher; |
42 | import org.springframework.batch.core.launch.JobParametersNotFoundException; |
43 | import org.springframework.beans.factory.BeanDefinitionStoreException; |
44 | import org.springframework.beans.factory.config.AutowireCapableBeanFactory; |
45 | import org.springframework.context.ConfigurableApplicationContext; |
46 | import org.springframework.context.support.ClassPathXmlApplicationContext; |
47 | import org.springframework.util.Assert; |
48 | import org.springframework.util.StringUtils; |
49 | |
50 | /** |
51 | * <p> |
52 | * Basic launcher for starting jobs from the command line. In general, it is |
53 | * assumed that this launcher will primarily be used to start a job via a script |
54 | * from an Enterprise Scheduler. Therefore, exit codes are mapped to integers so |
55 | * that schedulers can use the returned values to determine the next course of |
56 | * action. The returned values can also be useful to operations teams in |
57 | * determining what should happen upon failure. For example, a returned code of |
58 | * 5 might mean that some resource wasn't available and the job should be |
59 | * restarted. However, a code of 10 might mean that something critical has |
60 | * happened and the issue should be escalated. |
61 | * </p> |
62 | * |
63 | * <p> |
64 | * With any launch of a batch job within Spring Batch, a Spring context |
65 | * containing the {@link Job} and some execution context has to be created. This |
66 | * command line launcher can be used to load the job and its context from a |
67 | * single location. All dependencies of the launcher will then be satisfied by |
68 | * autowiring by type from the combined application context. Default values are |
69 | * provided for all fields except the {@link JobLauncher} and {@link JobLocator} |
70 | * . Therefore, if autowiring fails to set it (it should be noted that |
71 | * dependency checking is disabled because most of the fields have default |
72 | * values and thus don't require dependencies to be fulfilled via autowiring) |
73 | * then an exception will be thrown. It should also be noted that even if an |
74 | * exception is thrown by this class, it will be mapped to an integer and |
75 | * returned. |
76 | * </p> |
77 | * |
78 | * <p> |
79 | * Notice a property is available to set the {@link SystemExiter}. This class is |
80 | * used to exit from the main method, rather than calling System.exit() |
81 | * directly. This is because unit testing a class the calls System.exit() is |
82 | * impossible without kicking off the test within a new JVM, which it is |
83 | * possible to do, however it is a complex solution, much more so than |
84 | * strategizing the exiter. |
85 | * </p> |
86 | * |
87 | * <p> |
88 | * The arguments to this class are as follows: |
89 | * </p> |
90 | * |
91 | * <code> |
92 | * jobPath <options> jobName (jobParameters)* |
93 | * </code> |
94 | * |
95 | * <p> |
96 | * The command line options are as follows |
97 | * <ul> |
98 | * <li>jobPath: the xml application context containing a {@link Job} |
99 | * <li>-restart: (optional) to restart the last failed execution</li> |
100 | * <li>-next: (optional) to start the next in a sequence according to the |
101 | * {@link JobParametersIncrementer} in the {@link Job}</li> |
102 | * <li>jobName: the bean id of the job. |
103 | * <li>jobParameters: 0 to many parameters that will be used to launch a job. |
104 | * </ul> |
105 | * </p> |
106 | * |
107 | * <p> |
108 | * If the <code>-next</code> option is used the parameters on the command line |
109 | * (if any) are appended to those retrieved from the incrementer, overriding any |
110 | * with the same key. |
111 | * </p> |
112 | * |
113 | * <p> |
114 | * The combined application context must contain only one instance of |
115 | * {@link JobLauncher}. The job parameters passed in to the command line will be |
116 | * converted to {@link Properties} by assuming that each individual element is |
117 | * one parameter that is separated by an equals sign. For example, |
118 | * "vendor.id=290232". Below is an example arguments list: " |
119 | * |
120 | * <p> |
121 | * <code> |
122 | * java org.springframework.batch.execution.bootstrap.support.CommandLineJobRunner testJob.xml |
123 | * testJob schedule.date=2008/01/24 vendor.id=3902483920 |
124 | * <code> |
125 | * </p> |
126 | * |
127 | * <p> |
128 | * Once arguments have been successfully parsed, autowiring will be used to set |
129 | * various dependencies. The {@JobLauncher} for example, will be |
130 | * loaded this way. If none is contained in the bean factory (it searches by |
131 | * type) then a {@link BeanDefinitionStoreException} will be thrown. The same |
132 | * exception will also be thrown if there is more than one present. Assuming the |
133 | * JobLauncher has been set correctly, the jobName argument will be used to |
134 | * obtain an actual {@link Job}. If a {@link JobLocator} has been set, then it |
135 | * will be used, if not the beanFactory will be asked, using the jobName as the |
136 | * bean id. |
137 | * </p> |
138 | * |
139 | * @author Dave Syer |
140 | * @author Lucas Ward |
141 | * @since 1.0 |
142 | */ |
143 | public class CommandLineJobRunner { |
144 | |
145 | protected static final Log logger = LogFactory.getLog(CommandLineJobRunner.class); |
146 | |
147 | private ExitCodeMapper exitCodeMapper = new SimpleJvmExitCodeMapper(); |
148 | |
149 | private JobLauncher launcher; |
150 | |
151 | private JobLocator jobLocator; |
152 | |
153 | // Package private for unit test |
154 | private static SystemExiter systemExiter = new JvmSystemExiter(); |
155 | |
156 | private static String message = ""; |
157 | |
158 | private JobParametersConverter jobParametersConverter = new DefaultJobParametersConverter(); |
159 | |
160 | private JobExplorer jobExplorer; |
161 | |
162 | /** |
163 | * Injection setter for the {@link JobLauncher}. |
164 | * |
165 | * @param launcher the launcher to set |
166 | */ |
167 | public void setLauncher(JobLauncher launcher) { |
168 | this.launcher = launcher; |
169 | } |
170 | |
171 | /** |
172 | * Injection setter for {@link JobExplorer}. |
173 | * |
174 | * @param jobExplorer the {@link JobExplorer} to set |
175 | */ |
176 | public void setJobExplorer(JobExplorer jobExplorer) { |
177 | this.jobExplorer = jobExplorer; |
178 | } |
179 | |
180 | /** |
181 | * Injection setter for the {@link ExitCodeMapper}. |
182 | * |
183 | * @param exitCodeMapper the exitCodeMapper to set |
184 | */ |
185 | public void setExitCodeMapper(ExitCodeMapper exitCodeMapper) { |
186 | this.exitCodeMapper = exitCodeMapper; |
187 | } |
188 | |
189 | /** |
190 | * Static setter for the {@link SystemExiter} so it can be adjusted before |
191 | * dependency injection. Typically overridden by |
192 | * {@link #setSystemExiter(SystemExiter)}. |
193 | * |
194 | * @param systemExitor |
195 | */ |
196 | public static void presetSystemExiter(SystemExiter systemExiter) { |
197 | CommandLineJobRunner.systemExiter = systemExiter; |
198 | } |
199 | |
200 | /** |
201 | * Retrieve the error message set by an instance of |
202 | * {@link CommandLineJobRunner} as it exits. Empty if the last job launched |
203 | * was successful. |
204 | * |
205 | * @return the error message |
206 | */ |
207 | public static String getErrorMessage() { |
208 | return message; |
209 | } |
210 | |
211 | /** |
212 | * Injection setter for the {@link SystemExiter}. |
213 | * |
214 | * @param systemExitor |
215 | */ |
216 | public void setSystemExiter(SystemExiter systemExiter) { |
217 | CommandLineJobRunner.systemExiter = systemExiter; |
218 | } |
219 | |
220 | /** |
221 | * Injection setter for {@link JobParametersConverter}. |
222 | * |
223 | * @param jobParametersConverter |
224 | */ |
225 | public void setJobParametersConverter(JobParametersConverter jobParametersConverter) { |
226 | this.jobParametersConverter = jobParametersConverter; |
227 | } |
228 | |
229 | /** |
230 | * Delegate to the exiter to (possibly) exit the VM gracefully. |
231 | * |
232 | * @param status |
233 | */ |
234 | public void exit(int status) { |
235 | systemExiter.exit(status); |
236 | } |
237 | |
238 | /** |
239 | * {@link JobLocator} to find a job to run. |
240 | * @param jobLocator a {@link JobLocator} |
241 | */ |
242 | public void setJobLocator(JobLocator jobLocator) { |
243 | this.jobLocator = jobLocator; |
244 | } |
245 | |
246 | /* |
247 | * Start a job by obtaining a combined classpath using the job launcher and |
248 | * job paths. If a JobLocator has been set, then use it to obtain an actual |
249 | * job, if not ask the context for it. |
250 | */ |
251 | int start(String jobPath, String jobName, String[] parameters, Set<String> opts) { |
252 | |
253 | ConfigurableApplicationContext context = null; |
254 | |
255 | try { |
256 | context = new ClassPathXmlApplicationContext(jobPath); |
257 | context.getAutowireCapableBeanFactory().autowireBeanProperties(this, |
258 | AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, false); |
259 | |
260 | Assert.state(launcher != null, "A JobLauncher must be provided. Please add one to the configuration."); |
261 | if (opts.contains("-restart") || opts.contains("-next")) { |
262 | Assert |
263 | .state(jobExplorer != null, |
264 | "A JobExplorer must be provided for a restart or start next operation. Please add one to the configuration."); |
265 | } |
266 | |
267 | Job job; |
268 | if (jobLocator != null) { |
269 | job = jobLocator.getJob(jobName); |
270 | } |
271 | else { |
272 | job = (Job) context.getBean(jobName); |
273 | } |
274 | |
275 | JobParameters jobParameters = jobParametersConverter.getJobParameters(StringUtils |
276 | .splitArrayElementsIntoProperties(parameters, "=")); |
277 | Assert.isTrue(parameters == null || parameters.length == 0 || !jobParameters.isEmpty(), |
278 | "Invalid JobParameters " + Arrays.asList(parameters) |
279 | + ". If parameters are provided they should be in the form name=value (no whitespace)."); |
280 | |
281 | if (opts.contains("-restart")) { |
282 | jobParameters = getLastFailedJobParameters(jobName); |
283 | } |
284 | else if (opts.contains("-next")) { |
285 | JobParameters nextParameters = getNextJobParameters(job); |
286 | Map<String, JobParameter> map = new HashMap<String, JobParameter>(nextParameters.getParameters()); |
287 | map.putAll(jobParameters.getParameters()); |
288 | jobParameters = new JobParameters(map); |
289 | } |
290 | |
291 | JobExecution jobExecution = launcher.run(job, jobParameters); |
292 | return exitCodeMapper.intValue(jobExecution.getExitStatus().getExitCode()); |
293 | |
294 | } |
295 | catch (Throwable e) { |
296 | String message = "Job Terminated in error: " + e.getMessage(); |
297 | logger.error(message, e); |
298 | CommandLineJobRunner.message = message; |
299 | return exitCodeMapper.intValue(ExitStatus.FAILED.getExitCode()); |
300 | } |
301 | finally { |
302 | if (context != null) { |
303 | context.close(); |
304 | } |
305 | } |
306 | } |
307 | |
308 | /** |
309 | * @param jobName |
310 | * @return |
311 | * @throws JobParametersNotFoundException |
312 | */ |
313 | private JobParameters getLastFailedJobParameters(String jobName) throws JobParametersNotFoundException { |
314 | |
315 | int start = 0; |
316 | int count = 100; |
317 | List<JobInstance> lastInstances = jobExplorer.getJobInstances(jobName, start, count); |
318 | |
319 | JobParameters jobParameters = null; |
320 | |
321 | if (lastInstances.isEmpty()) { |
322 | throw new JobParametersNotFoundException("No job instance found for job=" + jobName); |
323 | } |
324 | |
325 | while (!lastInstances.isEmpty()) { |
326 | |
327 | for (JobInstance jobInstance : lastInstances) { |
328 | List<JobExecution> jobExecutions = jobExplorer.getJobExecutions(jobInstance); |
329 | if (jobExecutions == null || jobExecutions.isEmpty()) { |
330 | continue; |
331 | } |
332 | JobExecution jobExecution = jobExecutions.get(jobExecutions.size() - 1); |
333 | if (jobExecution.getStatus().isGreaterThan(BatchStatus.STOPPING)) { |
334 | jobParameters = jobInstance.getJobParameters(); |
335 | break; |
336 | } |
337 | } |
338 | |
339 | if (jobParameters != null) { |
340 | break; |
341 | } |
342 | |
343 | start += count; |
344 | lastInstances = jobExplorer.getJobInstances(jobName, start, count); |
345 | |
346 | } |
347 | |
348 | if (jobParameters == null) { |
349 | throw new JobParametersNotFoundException("No failed or stopped execution found for job=" + jobName); |
350 | } |
351 | return jobParameters; |
352 | |
353 | } |
354 | |
355 | /** |
356 | * @param job the job that we need to find the next parameters for |
357 | * @return the next job parameters if they can be located |
358 | * @throws JobParametersNotFoundException if there is a problem |
359 | */ |
360 | private JobParameters getNextJobParameters(Job job) throws JobParametersNotFoundException { |
361 | String jobName = job.getName(); |
362 | JobParameters jobParameters; |
363 | List<JobInstance> lastInstances = jobExplorer.getJobInstances(jobName, 0, 1); |
364 | |
365 | JobParametersIncrementer incrementer = job.getJobParametersIncrementer(); |
366 | if (incrementer == null) { |
367 | throw new JobParametersNotFoundException("No job parameters incrementer found for job=" + jobName); |
368 | } |
369 | |
370 | if (lastInstances.isEmpty()) { |
371 | jobParameters = incrementer.getNext(new JobParameters()); |
372 | if (jobParameters == null) { |
373 | throw new JobParametersNotFoundException("No bootstrap parameters found from incrementer for job=" |
374 | + jobName); |
375 | } |
376 | } |
377 | else { |
378 | jobParameters = incrementer.getNext(lastInstances.get(0).getJobParameters()); |
379 | } |
380 | return jobParameters; |
381 | } |
382 | |
383 | /** |
384 | * Launch a batch job using a {@link CommandLineJobRunner}. Creates a new |
385 | * Spring context for the job execution, and uses a common parent for all |
386 | * such contexts. No exception are thrown from this method, rather |
387 | * exceptions are logged and an integer returned through the exit status in |
388 | * a {@link JvmSystemExiter} (which can be overridden by defining one in the |
389 | * Spring context).<br/> |
390 | * Parameters can be provided in the form key=value, and will be converted |
391 | * using the injected {@link JobParametersConverter}. |
392 | * |
393 | * @param args <p> |
394 | * <ul> |
395 | * <li>-restart: (optional) if the job has failed or stopped and the most |
396 | * recent execution should be restarted</li> |
397 | * <li>-next: (optional) if the job has a {@link JobParametersIncrementer} |
398 | * that can be used to launch the next in a sequence</li> |
399 | * <li>jobPath: the xml application context containing a {@link Job} |
400 | * <li>jobName: the bean id of the job. |
401 | * <li>jobParameters: 0 to many parameters that will be used to launch a |
402 | * job. |
403 | * </ul> |
404 | * The options (<code>-restart, -next</code>) can occur anywhere in the |
405 | * command line. |
406 | * </p> |
407 | */ |
408 | public static void main(String[] args) { |
409 | |
410 | CommandLineJobRunner command = new CommandLineJobRunner(); |
411 | |
412 | Set<String> opts = new HashSet<String>(); |
413 | List<String> params = new ArrayList<String>(); |
414 | |
415 | int count = 0; |
416 | String jobPath = null; |
417 | String jobName = null; |
418 | |
419 | for (String arg : args) { |
420 | if (arg.startsWith("-")) { |
421 | opts.add(arg); |
422 | } |
423 | else { |
424 | switch (count) { |
425 | case 0: |
426 | jobPath = arg; |
427 | break; |
428 | case 1: |
429 | jobName = arg; |
430 | break; |
431 | default: |
432 | params.add(arg); |
433 | break; |
434 | } |
435 | count++; |
436 | } |
437 | } |
438 | |
439 | if (jobPath == null || jobName == null) { |
440 | String message = "At least 2 arguments are required: JobPath and JobName."; |
441 | logger.error(message); |
442 | CommandLineJobRunner.message = message; |
443 | command.exit(1); |
444 | } |
445 | |
446 | String[] parameters = params.toArray(new String[params.size()]); |
447 | |
448 | int result = command.start(jobPath, jobName, parameters, opts); |
449 | command.exit(result); |
450 | } |
451 | |
452 | } |