/* * Copyright 2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package de.codecentric.batch.web; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.batch.operations.JobExecutionAlreadyCompleteException; import javax.batch.operations.JobStartException; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.batch.core.BatchStatus; import org.springframework.batch.core.Job; import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.JobInstance; import org.springframework.batch.core.JobParameter; import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.JobParametersIncrementer; import org.springframework.batch.core.JobParametersInvalidException; import org.springframework.batch.core.UnexpectedJobExecutionException; import org.springframework.batch.core.configuration.JobRegistry; import org.springframework.batch.core.converter.DefaultJobParametersConverter; import org.springframework.batch.core.converter.JobParametersConverter; import org.springframework.batch.core.explore.JobExplorer; import org.springframework.batch.core.jsr.launch.JsrJobOperator; import org.springframework.batch.core.launch.JobExecutionNotRunningException; import org.springframework.batch.core.launch.JobInstanceAlreadyExistsException; import org.springframework.batch.core.launch.JobLauncher; import org.springframework.batch.core.launch.JobOperator; import org.springframework.batch.core.launch.JobParametersNotFoundException; import org.springframework.batch.core.launch.NoSuchJobException; import org.springframework.batch.core.launch.NoSuchJobExecutionException; 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.batch.support.PropertiesConverter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpStatus; import org.springframework.util.FileCopyUtils; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import de.codecentric.batch.logging.DefaultJobLogFileNameCreator; import de.codecentric.batch.logging.JobLogFileNameCreator; /** * Very simple REST-API for starting and stopping jobs and keeping track of its status. * Made for script interaction. * * <p>The base url can be set via property batch.web.operations.base, its default is /batch/operations. * There are four endpoints available: * * <ol> * <li>Starting jobs<br> * {base_url}/jobs/{jobName} / POST<br> * Optionally you may define job parameters via request param 'jobParameters'. If a JobParametersIncrementer * is specified in the job, it is used to increment the parameters.<br> * On success, it returns the JobExecution's id as a plain string.<br> * On failure, it returns the message of the Exception as a plain string. There are different failure * possibilities: * <ul> * <li>HTTP response code 404 (NOT_FOUND): the job cannot be found, not deployed on this server.</li> * <li>HTTP response code 409 (CONFLICT): the JobExecution already exists and is either running or not restartable.</li> * <li>HTTP response code 422 (UNPROCESSABLE_ENTITY): the job parameters didn't pass the validator.</li> * <li>HTTP response code 500 (INTERNAL_SERVER_ERROR): any other unexpected failure.</li> * </ul></li> * * <li>Retrieving an JobExecution's ExitCode<br> * {base_url}/jobs/executions/{executionId} / GET<br> * On success, it returns the ExitCode of the JobExecution specified by the executionId as a plain string.<br> * On failure, it returns the message of the Exception as a plain string. There are different failure * possibilities: * <ul> * <li>HTTP response code 404 (NOT_FOUND): the JobExecution cannot be found.</li> * <li>HTTP response code 500 (INTERNAL_SERVER_ERROR): any other unexpected failure.</li> * </ul></li> * * <li>Retrieving a log file for a specific JobExecution<br> * {base_url}/jobs/executions/{executionId}/log / GET<br> * On success, it returns the log file belonging to the run of the JobExecution specified by the executionId * as a plain string.<br> * On failure, it returns the message of the Exception as a plain string. There are different failure * possibilities: * <ul> * <li>HTTP response code 404 (NOT_FOUND): the log file cannot be found.</li> * <li>HTTP response code 500 (INTERNAL_SERVER_ERROR): any other unexpected failure.</li> * </ul></li> * * <li>Stopping jobs<br> * {base_url}/jobs/executions/{executionId} / DELETE<br> * On success, it returns true.<br> * On failure, it returns the message of the Exception as a plain string. There are different failure * possibilities: * <ul> * <li>HTTP response code 404 (NOT_FOUND): the JobExecution cannot be found.</li> * <li>HTTP response code 409 (CONFLICT): the JobExecution is not running.</li> * <li>HTTP response code 500 (INTERNAL_SERVER_ERROR): any other unexpected failure.</li> * </ul></li> * </ol> * * * @author Dennis Schulte * @author Tobias Flohre * */ @RestController @RequestMapping("${batch.web.operations.base:/batch/operations}") public class JobOperationsController { private static final Logger LOG = LoggerFactory.getLogger(JobOperationsController.class); public static final String JOB_PARAMETERS = "jobParameters"; private JobOperator jobOperator; private JobExplorer jobExplorer; private JobRegistry jobRegistry; private JobRepository jobRepository; private JobLauncher jobLauncher; private JsrJobOperator jsrJobOperator; private JobParametersConverter jobParametersConverter = new DefaultJobParametersConverter(); private JobLogFileNameCreator jobLogFileNameCreator = new DefaultJobLogFileNameCreator(); public JobOperationsController(JobOperator jobOperator, JobExplorer jobExplorer, JobRegistry jobRegistry, JobRepository jobRepository, JobLauncher jobLauncher, JsrJobOperator jsrJobOperator) { super(); this.jobOperator = jobOperator; this.jobExplorer = jobExplorer; this.jobRegistry = jobRegistry; this.jobRepository = jobRepository; this.jobLauncher = jobLauncher; this.jsrJobOperator = jsrJobOperator; } @RequestMapping(value = "/jobs/{jobName}", method = RequestMethod.POST) public String launch(@PathVariable String jobName, @RequestParam MultiValueMap<String, String> payload) throws NoSuchJobException, JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException, JobParametersNotFoundException { String parameters = payload.getFirst(JOB_PARAMETERS); if (LOG.isDebugEnabled()) { LOG.debug("Attempt to start job with name " + jobName + " and parameters "+parameters+"."); } try { Job job = jobRegistry.getJob(jobName); JobParameters jobParameters = createJobParametersWithIncrementerIfAvailable(parameters, job); Long id = jobLauncher.run(job, jobParameters).getId(); return String.valueOf(id); } catch (NoSuchJobException e){ // Job hasn't been found in normal context, so let's check if there's a JSR-352 job. String jobConfigurationLocation = "/META-INF/batch-jobs/" + jobName + ".xml"; Resource jobXml = new ClassPathResource(jobConfigurationLocation); if (!jobXml.exists()) { throw e; } else { Long id = jsrJobOperator.start(jobName, PropertiesConverter.stringToProperties(parameters)); return String.valueOf(id); } } } private JobParameters createJobParametersWithIncrementerIfAvailable(String parameters, Job job) throws JobParametersNotFoundException { JobParameters jobParameters = jobParametersConverter.getJobParameters(PropertiesConverter.stringToProperties(parameters)); // use JobParametersIncrementer to create JobParameters if incrementer is set and only if the job is no restart if (job.getJobParametersIncrementer() != null){ JobExecution lastJobExecution = jobRepository.getLastJobExecution(job.getName(), jobParameters); boolean restart = false; // check if job failed before if (lastJobExecution != null) { BatchStatus status = lastJobExecution.getStatus(); if (status.isUnsuccessful() && status != BatchStatus.ABANDONED) { restart = true; } } // if it's not a restart, create new JobParameters with the incrementer if (!restart) { JobParameters nextParameters = getNextJobParameters(job); Map<String, JobParameter> map = new HashMap<String, JobParameter>(nextParameters.getParameters()); map.putAll(jobParameters.getParameters()); jobParameters = new JobParameters(map); } } return jobParameters; } /** * Borrowed from CommandLineJobRunner. * @param job the job that we need to find the next parameters for * @return the next job parameters if they can be located * @throws JobParametersNotFoundException if there is a problem */ private JobParameters getNextJobParameters(Job job) throws JobParametersNotFoundException { String jobIdentifier = job.getName(); JobParameters jobParameters; List<JobInstance> lastInstances = jobExplorer.getJobInstances(jobIdentifier, 0, 1); JobParametersIncrementer incrementer = job.getJobParametersIncrementer(); if (lastInstances.isEmpty()) { jobParameters = incrementer.getNext(new JobParameters()); if (jobParameters == null) { throw new JobParametersNotFoundException("No bootstrap parameters found from incrementer for job=" + jobIdentifier); } } else { List<JobExecution> lastExecutions = jobExplorer.getJobExecutions(lastInstances.get(0)); jobParameters = incrementer.getNext(lastExecutions.get(0).getJobParameters()); } return jobParameters; } @RequestMapping(value = "/jobs/executions/{executionId}", method = RequestMethod.GET) public String getStatus(@PathVariable long executionId) throws NoSuchJobExecutionException { if (LOG.isDebugEnabled()) { LOG.debug("Get ExitCode for JobExecution with id: " + executionId+"."); } JobExecution jobExecution = jobExplorer.getJobExecution(executionId); if (jobExecution != null){ return jobExecution.getExitStatus().getExitCode(); } else { throw new NoSuchJobExecutionException("JobExecution with id "+executionId+" not found."); } } @RequestMapping(value = "/jobs/executions/{executionId}/log", method = RequestMethod.GET) public void getLogFile(HttpServletResponse response, @PathVariable long executionId) throws NoSuchJobExecutionException, IOException { if (LOG.isDebugEnabled()) { LOG.debug("Get log file for job with executionId: " + executionId); } String loggingPath = createLoggingPath(); JobExecution jobExecution = jobExplorer.getJobExecution(executionId); if (jobExecution == null){ throw new NoSuchJobExecutionException("JobExecution with id "+executionId+" not found."); } File downloadFile = new File(loggingPath+jobLogFileNameCreator.getName(jobExecution)); InputStream is = new FileInputStream(downloadFile); FileCopyUtils.copy(is, response.getOutputStream()); response.flushBuffer(); } private String createLoggingPath() { String loggingPath = System.getProperty("LOG_PATH"); if (loggingPath == null){ loggingPath = System.getProperty("java.io.tmpdir"); } if (loggingPath == null){ loggingPath = "/tmp"; } if (!loggingPath.endsWith("/")){ loggingPath = loggingPath+"/"; } return loggingPath; } @RequestMapping(value = "/jobs/executions/{executionId}", method = RequestMethod.DELETE) public String stop(@PathVariable long executionId) throws NoSuchJobExecutionException, JobExecutionNotRunningException { if (LOG.isDebugEnabled()) { LOG.debug("Stop JobExecution with id: " + executionId); } Boolean successful = jobOperator.stop(executionId); return successful.toString(); } @ResponseStatus(HttpStatus.NOT_FOUND) @ExceptionHandler({NoSuchJobException.class, NoSuchJobExecutionException.class, JobStartException.class}) public String handleNotFound(Exception ex) { LOG.warn("Job or JobExecution not found.",ex); return ex.getMessage(); } @ResponseStatus(HttpStatus.NOT_FOUND) @ExceptionHandler(JobParametersNotFoundException.class) public String handleNoBootstrapParametersCreatedByIncrementer(Exception ex) { LOG.warn("JobParametersIncrementer didn't provide bootstrap parameters.",ex); return ex.getMessage(); } @ResponseStatus(HttpStatus.CONFLICT) @ExceptionHandler({UnexpectedJobExecutionException.class, JobInstanceAlreadyExistsException.class, JobInstanceAlreadyCompleteException.class}) public String handleAlreadyExists(Exception ex) { LOG.warn("JobInstance or JobExecution already exists.",ex); return ex.getMessage(); } @ResponseStatus(HttpStatus.CONFLICT) @ExceptionHandler({JobExecutionAlreadyRunningException.class, JobExecutionAlreadyCompleteException.class, JobRestartException.class}) public String handleAlreadyRunningOrComplete(Exception ex) { LOG.warn("JobExecution already running or complete.",ex); return ex.getMessage(); } @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) @ExceptionHandler(JobParametersInvalidException.class) public String handleParametersInvalid(Exception ex) { LOG.warn("Job parameters are invalid.",ex); return ex.getMessage(); } @ResponseStatus(HttpStatus.NOT_FOUND) @ExceptionHandler(FileNotFoundException.class) public String handleFileNotFound(Exception ex) { LOG.warn("Logfile not found.",ex); return ex.getMessage(); } @ResponseStatus(HttpStatus.CONFLICT) @ExceptionHandler(JobExecutionNotRunningException.class) public String handleNotRunning(Exception ex) { LOG.warn("JobExecution is not running.",ex); return ex.getMessage(); } @Autowired(required=false) public void setJobLogFileNameCreator(JobLogFileNameCreator jobLogFileNameCreator) { this.jobLogFileNameCreator = jobLogFileNameCreator; } }