package io.oasp.module.batch.common.base; import java.util.ArrayList; import java.util.List; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.batch.core.BatchStatus; import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.configuration.JobLocator; import org.springframework.batch.core.converter.DefaultJobParametersConverter; import org.springframework.batch.core.converter.JobParametersConverter; import org.springframework.batch.core.launch.JobExecutionNotRunningException; import org.springframework.batch.core.launch.JobLauncher; import org.springframework.batch.core.launch.JobOperator; import org.springframework.batch.core.launch.support.CommandLineJobRunner; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.ExitCodeGenerator; import org.springframework.boot.SpringApplication; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.ResourceLoader; import org.springframework.util.StringUtils; /** * Launcher for launching batch jobs from the command line when Spring Boot is used. Similar to the * {@link CommandLineJobRunner}, which does not work very well with Spring Boot. * <p> * Do not use this class if Spring Boot is not used! * <p> * It expects the full class name of the Spring Boot configuration class to be used as first argument, the class/XML * file for configuring the job as second argument and the job name as third.<br> * Moreover parameters can be specified as further arguments (convention: key1=value1 key2=value2 ...). * <p> * Example:<br> * java io.oasp.module.batch.common.base.SpringBootBatchCommandLine io.oasp.gastronomy.restaurant.SpringBootBatchApp * classpath:config/app/batch/beans-productimport.xml productImportJob drinks.file=file:import/drinks.csv * date(date)=2015/12/20 * <p> * For stopping all running executions of a job, use the -stop option. * <p> * Example:<br> * java io.oasp.module.batch.common.base.SpringBootBatchCommandLine io.oasp.gastronomy.restaurant.SpringBootBatchApp * classpath:config/app/batch/beans-productimport.xml productImportJob -stop * * */ public class SpringBootBatchCommandLine { private static final Logger LOG = LoggerFactory.getLogger(SpringBootBatchCommandLine.class); private ResourceLoader resourceLoader = new DefaultResourceLoader(); public static enum Operation { START, STOP }; private JobLauncher launcher; private JobLocator locator; private JobParametersConverter parametersConverter; private JobOperator operator; public static void main(String[] args) throws Exception { if (args.length < 3) { handleIncorrectParameters(); return; } List<String> configurations = new ArrayList<>(2); configurations.add(args[0]); configurations.add(args[1]); List<String> parameters = new ArrayList<>(); Operation op = Operation.START; if (args.length > 3 && args[3].equalsIgnoreCase("-stop")) { if (args.length > 4) { handleIncorrectParameters(); return; } op = Operation.STOP; } else { for (int i = 3; i < args.length; i++) { parameters.add(args[i]); } } new SpringBootBatchCommandLine().execute(op, configurations, args[2], parameters); } private static void handleIncorrectParameters() { LOG.error("Incorrect parameters."); LOG.info("Usage:"); LOG.info("java io.oasp.module.batch.common.base.SpringBootBatchCommandLine" + " <SpringBootConfiguration> <BatchJobConfiguration>" + " <JobName> param1=value1 param2=value2 ..."); LOG.info("For stopping all running executions of a batch job:"); LOG.info("java io.oasp.module.batch.common.base.BatchCommandLine" + " <SpringBootConfiguration> <BatchJobConfiguration>" + " <JobName> -stop"); LOG.info("Example:"); LOG.info("java io.oasp.module.batch.common.base.SpringBootBatchCommandLine" + " io.oasp.gastronomy.restaurant.SpringBootBatchApp" + " classpath:config/app/batch/beans-productimport.xml" + " productImportJob drinks.file=file:import/drinks.csv" + " date(date)=2015/12/20"); } protected int getReturnCode(JobExecution jobExecution) { if (jobExecution.getStatus() != null && jobExecution.getStatus() == BatchStatus.COMPLETED) return 0; else return 1; } private Object getConfiguration(String stringRepresentation) { // try to load a source of Spring bean definitions: // 1. try to load it as a (JavaConfig) class // 2. if that fails: try to load it as XML resource try { return Class.forName(stringRepresentation); } catch (ClassNotFoundException e) { return this.resourceLoader.getResource(stringRepresentation); } } private void findBeans(ConfigurableApplicationContext ctx) { this.launcher = ctx.getBean(JobLauncher.class); this.locator = ctx.getBean(JobLocator.class); // supertype of JobRegistry this.operator = ctx.getBean(JobOperator.class); try { this.parametersConverter = ctx.getBean(JobParametersConverter.class); } catch (NoSuchBeanDefinitionException e) { this.parametersConverter = new DefaultJobParametersConverter(); } } /** * Initialize the application context and execute the operation. * <p> * The application context is closed after the operation has finished. * * @param operation The operation to start. * @param configurations The sources of bean configurations (either JavaConfig classes or XML files). * @param jobName The name of the job to launch/stop. * @param parameters The parameters (key=value). * @throws Exception */ public void execute(Operation operation, List<String> configurations, String jobName, List<String> parameters) throws Exception { // get sources of configuration Object[] configurationObjects = new Object[configurations.size()]; for (int i = 0; i < configurations.size(); i++) { configurationObjects[i] = getConfiguration(configurations.get(i)); } SpringApplication app = new SpringApplication(configurationObjects); // no (web) server needed app.setWebEnvironment(false); // start the application ConfigurableApplicationContext ctx = app.run(new String[0]); switch (operation) { case START: startBatch(ctx, jobName, parameters); break; case STOP: stopBatch(ctx, jobName); break; default: throw new RuntimeException("Unknown operation: " + operation); } } private void startBatch(ConfigurableApplicationContext ctx, String jobName, List<String> parameters) throws Exception { JobExecution jobExecution = null; try { findBeans(ctx); JobParameters params = this.parametersConverter .getJobParameters(StringUtils.splitArrayElementsIntoProperties(parameters.toArray(new String[] {}), "=")); // execute the batch // the JobOperator would require special logic for a restart, so we // are using the JobLauncher directly here jobExecution = this.launcher.run(this.locator.getJob(jobName), params); } finally { // evaluate the outcome final int returnCode = (jobExecution == null) ? 1 : getReturnCode(jobExecution); if (jobExecution == null) { LOG.error("Batch Status: Batch could not be started."); } else { LOG.info("Batch start time: {}", jobExecution.getStartTime() == null ? "null" : jobExecution.getStartTime()); LOG.info("Batch end time: {}", jobExecution.getEndTime() == null ? "null" : jobExecution.getEndTime()); if (returnCode == 0) { LOG.info("Batch Status: {}", jobExecution.getStatus() == null ? "null" : jobExecution.getStatus()); } else { LOG.error("Batch Status: {}", jobExecution.getStatus() == null ? "null" : jobExecution.getStatus()); } } LOG.info("Return Code: {}", returnCode); SpringApplication.exit(ctx, new ExitCodeGenerator() { @Override public int getExitCode() { return returnCode; } }); } } private void stopBatch(ConfigurableApplicationContext ctx, String jobName) throws Exception { int returnCode = 0; try { findBeans(ctx); Set<Long> runningJobExecutionIDs = this.operator.getRunningExecutions(jobName); if (runningJobExecutionIDs.isEmpty()) { throw new JobExecutionNotRunningException("Batch job " + jobName + " is currently not being executed."); } LOG.debug("Found {} executions to be stopped (potentially" + " already in state stopping).", runningJobExecutionIDs.size()); int stoppedCount = 0; for (Long id : runningJobExecutionIDs) { try { this.operator.stop(id); stoppedCount++; } catch (JobExecutionNotRunningException e) { // might have finished at this point // or was in state stopping already } } LOG.info("Actually stopped {} batch executions.", stoppedCount); } catch (Exception e) { returnCode = 1; throw e; } finally { final int returnCodeResult = returnCode; SpringApplication.exit(ctx, new ExitCodeGenerator() { @Override public int getExitCode() { return returnCodeResult; } }); } } }