/*
* Copyright 2009-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 org.springframework.batch.admin.web;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.TimeZone;
import javax.servlet.http.HttpServletRequest;
import org.springframework.batch.admin.domain.JobExecutionInfo;
import org.springframework.batch.admin.domain.JobInfo;
import org.springframework.batch.admin.domain.support.JobParametersExtractor;
import org.springframework.batch.admin.service.JobService;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobInstance;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersInvalidException;
import org.springframework.batch.core.launch.NoSuchJobException;
import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;
import org.springframework.batch.core.repository.JobRestartException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.ModelAttribute;
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.util.HtmlUtils;
/**
* Controller for listing and launching jobs.
*
* @author Dave Syer
* @author Michael Minella
*
*/
@Controller
public class JobController {
private final JobService jobService;
private Collection<String> extensions = new HashSet<String>();
private TimeZone timeZone = TimeZone.getDefault();
private JobParametersExtractor jobParametersExtractor = new JobParametersExtractor();
/**
* A collection of extensions that may be appended to request urls aimed at
* this controller.
*
* @param extensions the extensions (e.g. [rss, xml, atom])
*/
public void setExtensions(Collection<String> extensions) {
this.extensions = new LinkedHashSet<String>(extensions);
}
/**
* @param timeZone the timeZone to set
*/
@Autowired(required = false)
@Qualifier("userTimeZone")
public void setTimeZone(TimeZone timeZone) {
this.timeZone = timeZone;
}
@Autowired
public JobController(JobService jobService) {
super();
this.jobService = jobService;
extensions.addAll(Arrays.asList(".html", ".json", ".rss"));
}
@ModelAttribute("jobName")
public String getJobName(HttpServletRequest request) {
String path = request.getPathInfo();
if(path==null) {
path=request.getServletPath();
}
int index = path.lastIndexOf("jobs/") + 5;
if (index >= 0) {
path = path.substring(index);
}
if (!path.contains(".")) {
return path;
}
for (String extension : extensions) {
if (path.endsWith(extension)) {
path = StringUtils.stripFilenameExtension(path);
// Only remove one extension so a job can be called job.html and
// still be addressed
break;
}
}
return path;
}
@RequestMapping(value = "/jobs/{jobName}", method = RequestMethod.POST)
public String launch(ModelMap model, @ModelAttribute("jobName") String jobName,
@ModelAttribute("launchRequest") LaunchRequest launchRequest, Errors errors,
@RequestParam(defaultValue = "execution") String origin) {
launchRequest.setJobName(jobName);
String params = launchRequest.jobParameters;
JobParameters jobParameters = jobParametersExtractor.fromString(params);
try {
JobExecution jobExecution = jobService.launch(jobName, jobParameters);
model.addAttribute(new JobExecutionInfo(jobExecution, timeZone));
}
catch (NoSuchJobException e) {
errors.reject("no.such.job", new Object[] { jobName }, "No such job: " + jobName);
}
catch (JobExecutionAlreadyRunningException e) {
errors.reject("job.already.running", "A job with this name and parameters is already running.");
}
catch (JobRestartException e) {
errors.reject("job.could.not.restart", "The job was not able to restart.");
}
catch (JobInstanceAlreadyCompleteException e) {
errors.reject("job.already.complete", "A job with this name and parameters already completed successfully.");
}
catch (JobParametersInvalidException e) {
errors.reject("job.parameters.invalid", "The job parameters are invalid according to the configuration.");
}
if (!"job".equals(origin)) {
// if the origin is not specified we are probably not a UI client
return "jobs/execution";
}
else {
// In the UI we show the same page again...
return details(model, jobName, errors, 0, 20);
}
// Not a redirect because normally it is requested by an Ajax call so
// there's less of a pressing need for one (the browser history won't
// contain the request).
}
@RequestMapping(value = "/jobs/{jobName}", method = RequestMethod.GET)
public String details(ModelMap model, @ModelAttribute("jobName") String jobName, Errors errors,
@RequestParam(defaultValue = "0") int startJobInstance, @RequestParam(defaultValue = "20") int pageSize) {
boolean launchable = jobService.isLaunchable(jobName);
try {
Collection<JobInstance> result = jobService.listJobInstances(jobName, startJobInstance, pageSize);
Collection<JobInstanceInfo> jobInstances = new ArrayList<JobInstanceInfo>();
model.addAttribute("jobParameters", jobParametersExtractor.fromJobParameters(jobService.getLastJobParameters(jobName)));
for (JobInstance jobInstance : result) {
Collection<JobExecution> jobExecutions = jobService.getJobExecutionsForJobInstance(jobName, jobInstance.getId());
jobInstances.add(new JobInstanceInfo(jobInstance, jobExecutions, timeZone));
}
model.addAttribute("jobInstances", jobInstances);
int total = jobService.countJobInstances(jobName);
TableUtils.addPagination(model, total, startJobInstance, pageSize, "JobInstance");
int count = jobService.countJobExecutionsForJob(jobName);
model.addAttribute("jobInfo", new JobInfo(jobName, count, launchable, jobService.isIncrementable(jobName)));
}
catch (NoSuchJobException e) {
errors.reject("no.such.job", new Object[] { jobName },
"There is no such job (" + HtmlUtils.htmlEscape(jobName) + ")");
}
return "jobs/job";
}
@RequestMapping(value = "/jobs", method = RequestMethod.GET)
public void jobs(ModelMap model, @RequestParam(defaultValue = "0") int startJob,
@RequestParam(defaultValue = "20") int pageSize) {
int total = jobService.countJobs();
TableUtils.addPagination(model, total, startJob, pageSize, "Job");
Collection<String> names = jobService.listJobs(startJob, pageSize);
List<JobInfo> jobs = new ArrayList<JobInfo>();
for (String name : names) {
int count = 0;
try {
count = jobService.countJobExecutionsForJob(name);
}
catch (NoSuchJobException e) {
// shouldn't happen
}
boolean launchable = jobService.isLaunchable(name);
boolean incrementable = jobService.isIncrementable(name);
jobs.add(new JobInfo(name, count, null, launchable, incrementable));
}
model.addAttribute("jobs", jobs);
}
}