de.codecentric.batch.web.JobOperationsController.java Source code

Java tutorial

Introduction

Here is the source code for de.codecentric.batch.web.JobOperationsController.java

Source

/*
 * 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;
    }
}