/*
* Copyright 2014-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.batch.step.tasklet.x;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.StepExecutionListener;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.SimpleSystemProcessExitCodeMapper;
import org.springframework.batch.core.step.tasklet.SystemProcessExitCodeMapper;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
/**
* Abstract tasklet for running code in a separate process and capturing the log output. The step execution
* context will be updated with some runtime information as well as with the log output.
*
* Note: This this class is not thread-safe.
*
* @since 1.1
* @author Thomas Rrisberg
*/
@SuppressWarnings("rawtypes")
public abstract class AbstractProcessBuilderTasklet implements Tasklet, EnvironmentAware, StepExecutionListener {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
protected ConfigurableEnvironment environment;
/**
* Exit code of job
*/
protected int exitCode = -1;
private String exitMessage;
private boolean complete = false;
private boolean stopped = false;
private boolean stoppable = false;
private long checkInterval = 1000;
private JobExplorer jobExplorer;
private SystemProcessExitCodeMapper systemProcessExitCodeMapper = new SimpleSystemProcessExitCodeMapper();
private List<EnvironmentProvider> environmentProviders = new ArrayList<EnvironmentProvider>();
@Override
public void setEnvironment(Environment environment) {
this.environment = (ConfigurableEnvironment) environment;
}
public void addEnvironmentProvider(EnvironmentProvider environmentProvider) {
this.environmentProviders.add(environmentProvider);
}
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
StepExecution stepExecution = chunkContext.getStepContext().getStepExecution();
String commandDescription = getCommandDescription();
String commandName = getCommandName();
String commandDisplayString = getCommandDisplayString();
List<String> command = createCommand();
File out = File.createTempFile(commandName + "-", ".out");
stepExecution.getExecutionContext().putString(commandName.toLowerCase() + ".system.out", out.getAbsolutePath());
logger.info(commandName + " system.out: " + out.getAbsolutePath());
File err = File.createTempFile(commandName + "-", ".err");
stepExecution.getExecutionContext().putString(commandName.toLowerCase() + ".system.err", err.getAbsolutePath());
ProcessBuilder pb = new ProcessBuilder(command).redirectOutput(out).redirectError(err);
Map<String, String> env = pb.environment();
for (EnvironmentProvider envProvider : environmentProviders) {
envProvider.setEnvironment(env);
}
String msg = commandDescription + " is being launched";
stepExecution.getExecutionContext().putString(commandName.toLowerCase() + ".command", commandDisplayString.trim());
List<String> commandOut = new ArrayList<String>();
List<String> commandErr = new ArrayList<String>();
Process p = null;
try {
p = pb.start();
while (!complete) {
try {
exitCode = p.exitValue();
complete = true;
}
catch (IllegalThreadStateException e) {
if (stopped) {
p.destroy();
p = null;
break;
}
}
if (!complete) {
if (stoppable) {
JobExecution jobExecution =
jobExplorer.getJobExecution(stepExecution.getJobExecutionId());
if (jobExecution.isStopping()) {
stopped = true;
}
else if (chunkContext.getStepContext().getStepExecution().isTerminateOnly()) {
stopped = true;
}
}
Thread.sleep(checkInterval);
}
}
if (complete) {
msg = commandDescription + " finished with exit code: " + exitCode;
}
else {
msg = commandDescription + " was aborted due to a stop request";
}
if (complete && exitCode == 0) {
logger.info(msg);
}
else {
if (stopped) {
logger.warn(msg);
}
else {
logger.error(msg);
}
}
}
catch (IOException e) {
msg = commandDescription + " job failed with: " + e;
logger.error(msg);
}
catch (InterruptedException e) {
msg = commandDescription + " job failed with: " + e;
logger.error(msg);
}
finally {
if (p != null) {
p.destroy();
}
commandOut = getProcessOutput(out);
commandErr = getProcessOutput(err);
printLog(commandName, commandOut, commandErr);
String firstException = getFirstExceptionMessage(commandOut, commandErr);
if (firstException.length() > 0) {
msg = msg + " - " + firstException;
}
if (commandErr.size() > 0) {
StringBuilder commandLogErr = new StringBuilder();
for (String line : commandErr) {
commandLogErr.append(line).append("</br>");
}
stepExecution.getExecutionContext().putString(commandName.toLowerCase() + ".errors", commandLogErr.toString());
}
StringBuilder commandLogOut = new StringBuilder();
for (String line : commandOut) {
commandLogOut.append(line).append("</br>");
}
stepExecution.getExecutionContext().putString(commandName.toLowerCase() + ".log", commandLogOut.toString());
exitMessage = msg;
if (complete && exitCode != 0 || (!complete && !stopped)) {
if (firstException.length() > 0) {
throw new IllegalStateException("Step execution failed - " + firstException);
}
else {
throw new IllegalStateException("Step execution failed - " + msg);
}
}
}
return RepeatStatus.FINISHED;
}
@Override
public void beforeStep(StepExecution stepExecution) {
exitCode = -1;
complete = false;
stopped = false;
if (jobExplorer == null) {
stoppable = false;
}
else {
stoppable = isStoppable();
}
}
@Override
public ExitStatus afterStep(StepExecution stepExecution) {
if (complete) {
return systemProcessExitCodeMapper.getExitStatus(exitCode).addExitDescription(exitMessage);
}
else {
return ExitStatus.STOPPED.addExitDescription(exitMessage);
}
}
protected abstract boolean isStoppable();
protected abstract List<String> createCommand() throws Exception;
protected abstract String getCommandDisplayString();
protected abstract String getCommandName();
protected abstract String getCommandDescription();
protected List<String> getProcessOutput(File f) {
List<String> lines = new ArrayList<String>();
if (f == null) {
return lines;
}
FileInputStream in;
try {
in = new FileInputStream(f);
}
catch (FileNotFoundException e) {
lines.add("Failed to read log output due to " + e.getClass().getName());
lines.add(e.getMessage());
return lines;
}
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
try {
String line;
while ((line = reader.readLine()) != null) {
if (lines.size() < 10000) {
lines.add(line);
}
else {
lines.add("(log output truncated)");
break;
}
}
}
catch (IOException e) {
lines.add("Failed to read log output due to " + e.getClass().getName());
lines.add(e.getMessage());
}
finally {
try {
reader.close();
}
catch (IOException ignore) {}
}
return lines;
}
protected void printLog(String commandName, List<String> out, List<String> err) {
if ((complete && exitCode != 0) || (!complete && !stopped)) {
for (String line : err) {
logger.error(commandName + " err: " + line);
}
}
for (String line : out) {
if (!complete && !stopped) {
logger.warn(commandName + " log: " + line);
}
else {
if (logger.isDebugEnabled()) {
logger.debug(commandName + " log: " + line);
}
}
}
}
protected String getFirstExceptionMessage(List<String> out, List<String> err) {
String firstException = "";
List<String> log = new ArrayList<String>(out);
log.addAll(err);
for (String line : log) {
if (line.contains("Exception")) {
firstException = line;
break;
}
}
return firstException;
}
public void setJobExplorer(JobExplorer jobExplorer) {
this.jobExplorer = jobExplorer;
}
/**
* @param systemProcessExitCodeMapper maps system process return value to
* <code>ExitStatus</code> returned by Tasklet.
* {@link org.springframework.batch.core.step.tasklet.SimpleSystemProcessExitCodeMapper} is used by default.
*/
public void setSystemProcessExitCodeMapper(SystemProcessExitCodeMapper systemProcessExitCodeMapper) {
this.systemProcessExitCodeMapper = systemProcessExitCodeMapper;
}
/**
* The time interval how often the tasklet will check for termination
* status.
*
* @param checkInterval time interval in milliseconds (1 second by default).
*/
public void setTerminationCheckInterval(long checkInterval) {
this.checkInterval = checkInterval;
}
}