/*
* Copyright 2013-2015 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.xd.shell.command;
import static org.springframework.xd.shell.command.DeploymentOptionKeys.PROPERTIES_FILE_OPTION;
import static org.springframework.xd.shell.command.DeploymentOptionKeys.PROPERTIES_OPTION;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.springframework.batch.core.JobParameter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.PagedResources;
import org.springframework.shell.core.CommandMarker;
import org.springframework.shell.core.annotation.CliAvailabilityIndicator;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
import org.springframework.stereotype.Component;
import org.springframework.xd.rest.client.JobOperations;
import org.springframework.xd.rest.domain.JobDefinitionResource;
import org.springframework.xd.rest.domain.JobExecutionInfoResource;
import org.springframework.xd.rest.domain.JobInstanceInfoResource;
import org.springframework.xd.rest.domain.StepExecutionInfoResource;
import org.springframework.xd.rest.domain.StepExecutionProgressInfoResource;
import org.springframework.xd.rest.domain.support.DeploymentPropertiesFormat;
import org.springframework.xd.shell.Configuration;
import org.springframework.xd.shell.XDShell;
import org.springframework.xd.shell.command.support.JobCommandsUtils;
import org.springframework.xd.shell.util.Assertions;
import org.springframework.xd.shell.util.Table;
import org.springframework.xd.shell.util.TableHeader;
import org.springframework.xd.shell.util.TableRow;
import org.springframework.xd.shell.util.UiUtils;
/**
* Job commands.
*
* @author Glenn Renfro
* @author Ilayaperumal Gopinathan
* @author Gunnar Hillert
* @author Eric Bottard
*/
@Component
public class JobCommands implements CommandMarker {
private final static String CREATE_JOB = "job create";
private final static String LIST_JOBS = "job list";
private final static String LIST_JOB_EXECUTIONS = "job execution list";
private final static String LIST_STEP_EXECUTIONS = "job execution step list";
private final static String PROGRESS_STEP_EXECUTION = "job execution step progress";
private final static String DISPLAY_STEP_EXECUTION = "job execution step display";
private final static String DISPLAY_JOB_EXECUTION = "job execution display";
private final static String DISPLAY_JOB_INSTANCE = "job instance display";
private final static String STOP_ALL_JOB_EXECUTIONS = "job execution all stop";
private final static String STOP_JOB_EXECUTION = "job execution stop";
private final static String RESTART_JOB_EXECUTION = "job execution restart";
private final static String DEPLOY_JOB = "job deploy";
private final static String LAUNCH_JOB = "job launch";
private final static String UNDEPLOY_JOB = "job undeploy";
private final static String UNDEPLOY_ALL_JOBS = "job all undeploy";
private final static String DESTROY_JOB = "job destroy";
private final static String DESTROY_ALL_JOBS = "job all destroy";
@Autowired
private UserInput userInput;
@Autowired
private Configuration configuration;
@Autowired
private XDShell xdShell;
@CliAvailabilityIndicator({ CREATE_JOB, LIST_JOBS, DEPLOY_JOB, UNDEPLOY_JOB, DESTROY_JOB, STOP_JOB_EXECUTION,
DESTROY_ALL_JOBS, UNDEPLOY_ALL_JOBS, STOP_ALL_JOB_EXECUTIONS, DISPLAY_JOB_EXECUTION, LIST_JOB_EXECUTIONS,
RESTART_JOB_EXECUTION, DISPLAY_STEP_EXECUTION, LIST_STEP_EXECUTIONS, PROGRESS_STEP_EXECUTION,
DISPLAY_JOB_INSTANCE, LAUNCH_JOB })
public boolean available() {
return xdShell.getSpringXDOperations() != null;
}
@CliCommand(value = CREATE_JOB, help = "Create a job")
public String createJob(
@CliOption(mandatory = true, key = { "name", "" }, help = "the name to give to the job") String name,
@CliOption(mandatory = true, key = "definition", optionContext = "completion-job disable-string-converter", help = "job definition using xd dsl ") String dsl,
@CliOption(key = "deploy", help = "whether to deploy the job immediately", unspecifiedDefaultValue = "false", specifiedDefaultValue = "true") boolean deploy
) {
jobOperations().createJob(name, dsl, deploy);
return String.format((deploy ? "Successfully created and deployed job '%s'"
: "Successfully created job '%s'"), name);
}
@CliCommand(value = LIST_JOBS, help = "List all jobs")
public Table listJobs() {
final PagedResources<JobDefinitionResource> jobs = jobOperations().list();
final Table table = new Table()
.addHeader(1, new TableHeader("Job Name"))
.addHeader(2, new TableHeader("Job Definition"))
.addHeader(3, new TableHeader("Status"));
for (JobDefinitionResource jobDefinitionResource : jobs) {
table.newRow()
.addValue(1, jobDefinitionResource.getName())
.addValue(2, jobDefinitionResource.getDefinition())
.addValue(3, jobDefinitionResource.getStatus());
}
return table;
}
@CliCommand(value = LIST_JOB_EXECUTIONS, help = "List all job executions")
public Table listJobExecutions() {
final PagedResources<JobExecutionInfoResource> jobExecutions = jobOperations().listJobExecutions();
return createJobExecutionsTable(jobExecutions.getContent());
}
private Table createJobExecutionsTable(Collection<JobExecutionInfoResource> jobExecutions) {
final Table table = new Table();
table.addHeader(1, new TableHeader("Id"))
.addHeader(2, new TableHeader("Job Name"))
.addHeader(3, new TableHeader("Start Time"))
.addHeader(4, new TableHeader("Step Execution Count"))
.addHeader(5, new TableHeader("Execution Status"))
.addHeader(6, new TableHeader("Deployment Status"))
.addHeader(7, new TableHeader("Definition Status"))
.addHeader(8, new TableHeader("Composed"));
for (JobExecutionInfoResource jobExecutionInfoResource : jobExecutions) {
final TableRow row = new TableRow();
final String startTimeAsString = this.configuration.getLocalTime(jobExecutionInfoResource.getJobExecution().getStartTime());
row.addValue(1, String.valueOf(jobExecutionInfoResource.getExecutionId()))
.addValue(2, jobExecutionInfoResource.getName())
.addValue(3, startTimeAsString)
.addValue(4, String.valueOf(jobExecutionInfoResource.getStepExecutionCount()))
.addValue(5, jobExecutionInfoResource.getJobExecution().getStatus().name())
.addValue(6, (jobExecutionInfoResource.isDeployed()) ? "Deployed" : "Undeployed")
.addValue(7, (jobExecutionInfoResource.isDeleted()) ? "Destroyed" : "Exists")
.addValue(8, (jobExecutionInfoResource.isComposedJob()) ? " *" : "");
table.getRows().add(row);
}
return table;
}
@CliCommand(value = LIST_STEP_EXECUTIONS, help = "List all step executions for the provided job execution id")
public Table listStepExecutions(
@CliOption(mandatory = true, key = { "", "id" }, help = "the id of the job execution") long jobExecutionId) {
final List<StepExecutionInfoResource> stepExecutions = jobOperations().listStepExecutions(jobExecutionId);
final Table table = new Table();
table.addHeader(1, new TableHeader("Id"))
.addHeader(2, new TableHeader("Step Name"))
.addHeader(3, new TableHeader("Job Exec ID"))
.addHeader(4, new TableHeader("Start Time"))
.addHeader(5, new TableHeader("End Time"))
.addHeader(6, new TableHeader("Status"));
for (StepExecutionInfoResource stepExecutionInfoResource : stepExecutions) {
final String localStartTime = this.configuration.getLocalTime(stepExecutionInfoResource.getStepExecution().getStartTime());
final Date endTimeDate = stepExecutionInfoResource.getStepExecution().getEndTime();
final String localEndTime = (endTimeDate == null) ? "" : this.configuration.getLocalTime(endTimeDate);
final TableRow row = new TableRow();
row.addValue(1, String.valueOf(stepExecutionInfoResource.getStepExecution().getId()))
.addValue(2, stepExecutionInfoResource.getStepExecution().getStepName())
.addValue(3, String.valueOf(stepExecutionInfoResource.getJobExecutionId()))
.addValue(4, localStartTime)
.addValue(5, localEndTime)
.addValue(6, stepExecutionInfoResource.getStepExecution().getStatus().name());
table.getRows().add(row);
}
return table;
}
@CliCommand(value = PROGRESS_STEP_EXECUTION, help = "Get the progress info for the given step execution")
public Table stepExecutionProgress(
@CliOption(mandatory = true, key = { "", "id" }, help = "the id of the step execution") long stepExecutionId,
@CliOption(mandatory = true, key = { "jobExecutionId" }, help = "the job execution id") long jobExecutionId) {
StepExecutionProgressInfoResource progressInfoResource = jobOperations().stepExecutionProgress(jobExecutionId,
stepExecutionId);
Table table = new Table();
table.addHeader(1, new TableHeader("Id"))
.addHeader(2, new TableHeader("Step Name"))
.addHeader(3, new TableHeader("Percentage Complete"))
.addHeader(4, new TableHeader("Duration"));
final TableRow tableRow = new TableRow();
tableRow.addValue(1, String.valueOf(progressInfoResource.getStepExecution().getId()))
.addValue(2, String.valueOf(progressInfoResource.getStepExecution().getStepName()))
.addValue(3, String.format("%.0f%%", progressInfoResource.getPercentageComplete() * 100))
.addValue(4, String.format("%.0f ms", progressInfoResource.getDuration()));
table.getRows().add(tableRow);
return table;
}
@CliCommand(value = DISPLAY_STEP_EXECUTION, help = "Display the details of a Step Execution")
public Table displayStepExecution(
@CliOption(mandatory = true, key = { "", "id" }, help = "the id of the step execution") long stepExecutionId,
@CliOption(mandatory = true, key = { "jobExecutionId" }, help = "the job execution id") long jobExecutionId) {
final StepExecutionInfoResource stepExecutionInfoResource = jobOperations().displayStepExecution(
jobExecutionId,
stepExecutionId);
final Table stepExecutionTable = JobCommandsUtils.prepareStepExecutionTable(stepExecutionInfoResource,
this.configuration.getClientTimeZone());
return stepExecutionTable;
}
@CliCommand(value = DISPLAY_JOB_EXECUTION, help = "Display the details of a Job Execution")
public String display(
@CliOption(mandatory = true, key = { "", "id" }, help = "the id of the job execution") long jobExecutionId) {
final JobExecutionInfoResource jobExecutionInfoResource = jobOperations().displayJobExecution(jobExecutionId);
final Table jobExecutionTable = new Table();
jobExecutionTable.addHeader(1, new TableHeader("Property"))
.addHeader(2, new TableHeader("Value"));
final StringBuilder details = new StringBuilder();
details.append("Job Execution Details:\n");
details.append(UiUtils.HORIZONTAL_LINE);
final String localCreateTime = this.configuration.getLocalTime(jobExecutionInfoResource.getJobExecution().getCreateTime());
final String localStartTime = this.configuration.getLocalTime(jobExecutionInfoResource.getJobExecution().getStartTime());
final Date endTimeDate = jobExecutionInfoResource.getJobExecution().getEndTime();
final String localEndTime = (endTimeDate == null) ? "" : this.configuration.getLocalTime(endTimeDate);
jobExecutionTable.addRow("Job Execution ID", String.valueOf(jobExecutionInfoResource.getExecutionId()))
.addRow("Job Name", jobExecutionInfoResource.getName())
.addRow("Job Instance", String.valueOf(jobExecutionInfoResource.getJobExecution().getJobInstance().getId()))
.addRow("Composed Job", String.valueOf(jobExecutionInfoResource.isComposedJob()))
.addRow("Create Time", localCreateTime)
.addRow("Start Time", localStartTime)
.addRow("End Time", localEndTime)
.addRow("Running", String.valueOf(jobExecutionInfoResource.getJobExecution().isRunning()))
.addRow("Stopping", String.valueOf(jobExecutionInfoResource.getJobExecution().isStopping()))
.addRow("Step Execution Count", String.valueOf(jobExecutionInfoResource.getStepExecutionCount()))
.addRow("Execution Status", jobExecutionInfoResource.getJobExecution().getStatus().name());
details.append(jobExecutionTable);
details.append(UiUtils.HORIZONTAL_LINE);
details.append("Job Parameters:\n");
details.append(UiUtils.HORIZONTAL_LINE);
if (jobExecutionInfoResource.getJobExecution().getJobParameters().isEmpty()) {
details.append("No Job Parameters are present");
}
else {
final Table jobParameterTable = new Table();
jobParameterTable.addHeader(1, new TableHeader("Name"))
.addHeader(2, new TableHeader("Value"))
.addHeader(3, new TableHeader("Type"))
.addHeader(4, new TableHeader("Identifying"));
for (Map.Entry<String, JobParameter> jobParameterEntry : jobExecutionInfoResource.getJobExecution().getJobParameters().getParameters().entrySet()) {
jobParameterTable.addRow(jobParameterEntry.getKey(),
jobParameterEntry.getValue().getValue().toString(),
jobParameterEntry.getValue().getType().name(),
String.valueOf(jobParameterEntry.getValue().isIdentifying()));
}
details.append(jobParameterTable);
}
final String jobDefinitionStatus;
if (jobExecutionInfoResource.isDeleted()) {
jobDefinitionStatus = "Deleted";
}
else if (!jobExecutionInfoResource.isDeleted() && !jobExecutionInfoResource.isDeployed()) {
jobDefinitionStatus = "Undeployed";
}
else {
jobDefinitionStatus = "Deployed";
}
details.append(UiUtils.HORIZONTAL_LINE);
details.append("Underlying Job Definition Status: " + jobDefinitionStatus + "\n");
details.append(UiUtils.HORIZONTAL_LINE);
if (!jobExecutionInfoResource.getChildJobExecutions().isEmpty()) {
details.append("Composed Job - Child Job Executions:\n");
details.append(UiUtils.HORIZONTAL_LINE);
details.append(createJobExecutionsTable(jobExecutionInfoResource.getChildJobExecutions()));
}
return details.toString();
}
@CliCommand(value = STOP_ALL_JOB_EXECUTIONS, help = "Stop all the job executions that are running")
public String stopAllJobExecutions(
@CliOption(key = "force", help = "bypass confirmation prompt", unspecifiedDefaultValue = "false", specifiedDefaultValue = "true") boolean force) {
if (force || "y".equalsIgnoreCase(userInput.promptWithOptions("Really stop all job executions?", "n", "y", "n"))) {
jobOperations().stopAllJobExecutions();
return String.format("Stopped all the job executions");
}
else {
return "";
}
}
@CliCommand(value = STOP_JOB_EXECUTION, help = "Stop a job execution that is running")
public String stopJobExecution(
@CliOption(key = { "", "id" }, help = "the id of the job execution", mandatory = true) long executionId) {
jobOperations().stopJobExecution(executionId);
return String.format("Stopped Job execution that has executionId '%s'", executionId);
}
@CliCommand(value = RESTART_JOB_EXECUTION, help = "Restart a job that failed or interrupted previously")
public String restartJobExecution(
@CliOption(key = { "", "id" }, help = "the id of the job execution that failed or interrupted", mandatory = true) long executionId) {
jobOperations().restartJobExecution(executionId);
return String.format("Restarted Job execution that had executionId '%s'", executionId);
}
@CliCommand(value = DEPLOY_JOB, help = "Deploy a previously created job")
public String deployJob(
@CliOption(key = { "", "name" }, help = "the name of the job to deploy", mandatory = true, optionContext = "existing-job undeployed disable-string-converter") String name,
@CliOption(key = { PROPERTIES_OPTION }, help = "the properties for this deployment", mandatory = false) String properties,
@CliOption(key = { PROPERTIES_FILE_OPTION }, help = "the properties for this deployment (as a File)", mandatory = false) File propertiesFile
) throws IOException {
int which = Assertions.atMostOneOf(PROPERTIES_OPTION, properties, PROPERTIES_FILE_OPTION, propertiesFile);
Map<String, String> propertiesToUse;
switch (which) {
case 0:
propertiesToUse = DeploymentPropertiesFormat.parseDeploymentProperties(properties);
break;
case 1:
Properties props = new Properties();
try (FileInputStream fis = new FileInputStream(propertiesFile)) {
props.load(fis);
}
propertiesToUse = DeploymentPropertiesFormat.convert(props);
break;
case -1: // Neither option specified
propertiesToUse = Collections.<String, String> emptyMap();
break;
default:
throw new AssertionError();
}
jobOperations().deploy(name, propertiesToUse);
return String.format("Deployed job '%s'", name);
}
@CliCommand(value = LAUNCH_JOB, help = "Launch previously deployed job")
public String launchJob(
@CliOption(key = { "", "name" }, help = "the name of the job to deploy", optionContext = "existing-job disable-string-converter") String name,
@CliOption(key = { "params" }, help = "the parameters for the job", unspecifiedDefaultValue = "") String jobParameters) {
jobOperations().launchJob(name, jobParameters);
return String.format("Successfully submitted launch request for job '%s'", name);
}
@CliCommand(value = UNDEPLOY_JOB, help = "Un-deploy an existing job")
public String undeployJob(
@CliOption(key = { "", "name" }, help = "the name of the job to un-deploy", mandatory = true, optionContext = "existing-job deployed disable-string-converter") String name
) {
jobOperations().undeploy(name);
return String.format("Un-deployed Job '%s'", name);
}
@CliCommand(value = UNDEPLOY_ALL_JOBS, help = "Un-deploy all existing jobs")
public String undeployAllJobs(
@CliOption(key = "force", help = "bypass confirmation prompt", unspecifiedDefaultValue = "false", specifiedDefaultValue = "true") boolean force
) {
if (force || "y".equalsIgnoreCase(userInput.promptWithOptions("Really undeploy all jobs?", "n", "y", "n"))) {
jobOperations().undeployAll();
return String.format("Un-deployed all the jobs");
}
else {
return "";
}
}
@CliCommand(value = DISPLAY_JOB_INSTANCE, help = "Display information about a given job instance")
public CharSequence displayJobInstance(
@CliOption(key = { "", "id" }, help = "the id of the job instance to retrieve") long instanceId
) {
JobInstanceInfoResource jobInstance = jobOperations().displayJobInstance(instanceId);
StringBuilder result = new StringBuilder("Information about instance ");
result.append(jobInstance.getInstanceId()).append(" of the job '").append(jobInstance.getJobName()).append(
"':\n");
Table table = new Table();
table.addHeader(1, new TableHeader("Name"))
.addHeader(2, new TableHeader("Execution Id"))
.addHeader(3, new TableHeader("Start Time"))
.addHeader(4, new TableHeader("Step Execution Count"))
.addHeader(5, new TableHeader("Status"))
.addHeader(6, new TableHeader("Job Parameters"));
for (JobExecutionInfoResource jobExecutionInfoResource : jobInstance.getJobExecutions()) {
String startTimeAsString =
jobExecutionInfoResource.getStartDate() + " " +
jobExecutionInfoResource.getStartTime() + " " +
jobExecutionInfoResource.getTimeZone().getID();
table.addRow(//
jobInstance.getJobName(), //
String.valueOf(jobExecutionInfoResource.getExecutionId()), //
startTimeAsString, //
String.valueOf(jobExecutionInfoResource.getStepExecutionCount()), //
jobExecutionInfoResource.getJobExecution().getStatus().name(), //
jobExecutionInfoResource.getJobParametersString()//
);
}
result.append(table);
return result;
}
@CliCommand(value = DESTROY_JOB, help = "Destroy an existing job")
public String destroyJob(
@CliOption(key = { "", "name" }, help = "the name of the job to destroy", mandatory = true, optionContext = "existing-job disable-string-converter") String name
) {
jobOperations().destroy(name);
return String.format("Destroyed job '%s'", name);
}
@CliCommand(value = DESTROY_ALL_JOBS, help = "Destroy all existing jobs")
public String destroyAllJobs(
@CliOption(key = "force", help = "bypass confirmation prompt", unspecifiedDefaultValue = "false", specifiedDefaultValue = "true") boolean force
) {
if (force || "y".equalsIgnoreCase(userInput.promptWithOptions("Really destroy all jobs?", "n", "y", "n"))) {
jobOperations().destroyAll();
return String.format("Destroyed all the jobs");
}
else {
return "";
}
}
private JobOperations jobOperations() {
return xdShell.getSpringXDOperations().jobOperations();
}
}