* Copyright 2018 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,
 * See the License for the specific language governing permissions and
 * limitations under the License.

package de.codecentric.batch.web;

import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobOperator;
import org.springframework.batch.core.launch.NoSuchJobExecutionException;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.http.HttpStatus;
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.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import de.codecentric.batch.monitoring.RunningExecutionTracker;

 * Controller for delivering monitoring information, like
 * <ul>
 * <li>which jobs are deployed?</li>
 * <li>which jobs are currently running on this machine?</li>
 * <li>detailed information on any running or finished job.</li>
 * </ul>
 * <p>
 * The base url can be set via property batch.web.monitoring.base, its default is /batch/monitoring.
 * There are four endpoints available:
 * <ol>
 * <li>Retrieving the names of deployed jobs<br>
 * {base_url}/jobs / GET<br>
 * On success, it returns a JSON array of String containing the names of the deployed jobs.</li>
 * <li>Retrieving the ids of JobExecutions running on this server<br>
 * {base_url}/jobs/runningexecutions / GET<br>
 * On success, it returns a JSON array containing the ids of the JobExecutions running on this server.</li>
 * <li>Retrieving the ids of JobExecutions running on this server for a certain job name<br>
 * {base_url}/jobs/runningexecutions/{jobName} / GET<br>
 * On success, it returns a JSON array containing the ids of the JobExecutions running on this server belonging to the
 * specified job.</li>
 * <li>Retrieving the JobExecution<br>
 * {base_url}/jobs/executions/{executionId} / GET<br>
 * On success, it returns a JSON representation of the JobExecution specified by the id. This representation contains
 * everything you need to know about that job, from job name and BatchStatus to the number of processed items and time
 * used and so on.<br>
 * If the JobExecution cannot be found, a HTTP response code 404 is returned.</li>
 * </ol>
 * @author Tobias Flohre
public class JobMonitoringController {

	private static final Logger LOG = LoggerFactory.getLogger(JobMonitoringController.class);

	private JobOperator jobOperator;

	private JobExplorer jobExplorer;

	private RunningExecutionTracker runningExecutionTracker;

	public JobMonitoringController(JobOperator jobOperator, JobExplorer jobExplorer,
			RunningExecutionTracker runningExecutionTracker) {
		this.jobOperator = jobOperator;
		this.jobExplorer = jobExplorer;
		this.runningExecutionTracker = runningExecutionTracker;

	@RequestMapping(value = "/jobs", method = RequestMethod.GET)
	public Set<String> findRegisteredJobs() throws IOException {
		Set<String> registeredJobs = new HashSet<>(jobOperator.getJobNames());
		// Add JSR-352 jobs
		ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
		Resource[] xmlConfigurations = resourcePatternResolver.getResources("classpath*:/META-INF/batch-jobs/*.xml");
		for (Resource resource : xmlConfigurations) {
			registeredJobs.add(resource.getFilename().substring(0, resource.getFilename().length() - 4));
		return registeredJobs;

	@RequestMapping(value = "/jobs/runningexecutions", method = RequestMethod.GET)
	public Set<Long> findAllRunningExecutions() {
		return runningExecutionTracker.getAllRunningExecutionIds();

	@RequestMapping(value = "/jobs/runningexecutions/{jobName}", method = RequestMethod.GET)
	public Set<Long> findRunningExecutionsForJobName(@PathVariable String jobName) {
		return runningExecutionTracker.getRunningExecutionIdsForJobName(jobName);

	@RequestMapping(value = "/jobs/executions/{executionId}", method = RequestMethod.GET)
	public JobExecution findExecution(@PathVariable long executionId) throws NoSuchJobExecutionException {
		JobExecution jobExecution = jobExplorer.getJobExecution(executionId);
		if (jobExecution == null) {
			throw new NoSuchJobExecutionException("JobExecution with id " + executionId + " not found.");
		return jobExecution;

	public String handleNotFound(Exception ex) {
		LOG.warn("JobExecution not found.", ex);
		return ex.getMessage();