OPEN - BATCH-152: Add facility for calling system commands via a Tasklet
added SystemCommandTasklet to samples (example usage in taskletJob)
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
package org.springframework.batch.sample.tasklet;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.batch.repeat.ExitStatus;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Maps exit codes to {@link org.springframework.batch.repeat.ExitStatus}
|
||||
* according to injected map. The injected map is required to contain a value
|
||||
* for 'else' key, this value will be returned if the injected map
|
||||
* does not contain value for the exit code returned by the system process.
|
||||
*
|
||||
* @author Robert Kasanicky
|
||||
*/
|
||||
public class ConfigurableSystemProcessExitCodeMapper implements SystemProcessExitCodeMapper {
|
||||
|
||||
public static final String ELSE_KEY = "else";
|
||||
|
||||
private Map mappings;
|
||||
|
||||
public ExitStatus getExitStatus(int exitCode) {
|
||||
ExitStatus exitStatus = (ExitStatus) mappings.get(new Integer(exitCode));
|
||||
if (exitStatus != null) {
|
||||
return exitStatus;
|
||||
} else {
|
||||
return (ExitStatus) mappings.get(ELSE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mappings <code>Integer</code> exit code keys to
|
||||
* {@link org.springframework.batch.repeat.ExitStatus} values.
|
||||
*/
|
||||
public void setMappings(Map mappings) {
|
||||
Assert.notNull(mappings.get(ELSE_KEY));
|
||||
this.mappings = mappings;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.springframework.batch.sample.tasklet;
|
||||
|
||||
import org.springframework.batch.repeat.ExitStatus;
|
||||
|
||||
/**
|
||||
* Simple {@link SystemProcessExitCodeMapper} implementation that performs following mapping:
|
||||
*
|
||||
* 0 -> ExitStatus.FINISHED
|
||||
* else -> ExitStatus.FAILED
|
||||
*
|
||||
* @author Robert Kasanicky
|
||||
*/
|
||||
public class SimpleSystemProcessExitCodeMapper implements SystemProcessExitCodeMapper {
|
||||
public ExitStatus getExitStatus(int exitCode) {
|
||||
if (exitCode == 0) {
|
||||
return ExitStatus.FINISHED;
|
||||
} else {
|
||||
return ExitStatus.FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.springframework.batch.sample.tasklet;
|
||||
|
||||
/**
|
||||
* Exception indicating failed execution of system command.
|
||||
*/
|
||||
public class SystemCommandException extends RuntimeException {
|
||||
|
||||
// generated
|
||||
private static final long serialVersionUID = 5139355923336176733L;
|
||||
|
||||
public SystemCommandException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public SystemCommandException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package org.springframework.batch.sample.tasklet;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.batch.core.step.tasklet.Tasklet;
|
||||
import org.springframework.batch.repeat.ExitStatus;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link Tasklet} that executes a system command.
|
||||
*
|
||||
* The system command is executed in a new thread - timeout value is required to
|
||||
* be set, so that the batch job does not hang forever if the external process
|
||||
* hangs.
|
||||
*
|
||||
* @author Robert Kasanicky
|
||||
*/
|
||||
public class SystemCommandTasklet implements Tasklet, InitializingBean {
|
||||
|
||||
private String command;
|
||||
|
||||
private String[] environmentParams = null;
|
||||
|
||||
private File workingDirectory = null;
|
||||
|
||||
private SystemProcessExitCodeMapper systemProcessExitCodeMapper = new SimpleSystemProcessExitCodeMapper();
|
||||
|
||||
private long timeout = 0;
|
||||
|
||||
/**
|
||||
* Execute system command and map its exit code to {@link ExitStatus} using
|
||||
* {@link SystemProcessExitCodeMapper}.
|
||||
*/
|
||||
public ExitStatus execute() throws Exception {
|
||||
ExecutorThread executorThread = new ExecutorThread();
|
||||
executorThread.start();
|
||||
executorThread.join(timeout);
|
||||
|
||||
if (executorThread.finishedSuccessfully) {
|
||||
return systemProcessExitCodeMapper.getExitStatus(executorThread.exitCode);
|
||||
}
|
||||
else {
|
||||
executorThread.interrupt();
|
||||
throw new SystemCommandException(
|
||||
"Execution of system command failed (did not finish successfully within the timeout)");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param command command to be executed in a separate system process
|
||||
*/
|
||||
public void setCommand(String command) {
|
||||
this.command = command;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param envp environment parameter values, inherited from parent process
|
||||
* when not set (or set to null).
|
||||
*/
|
||||
public void setEnvironmentParams(String[] envp) {
|
||||
this.environmentParams = envp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param dir working directory of the spawned process, inherited from
|
||||
* parent process when not set (or set to null).
|
||||
*/
|
||||
public void setWorkingDirectory(String dir) {
|
||||
if (dir == null) {
|
||||
this.workingDirectory = null;
|
||||
return;
|
||||
}
|
||||
this.workingDirectory = new File(dir);
|
||||
Assert.isTrue(workingDirectory.exists(), "working directory must exist");
|
||||
Assert.isTrue(workingDirectory.isDirectory(), "working directory value must be a directory");
|
||||
|
||||
}
|
||||
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
Assert.hasLength(command, "'command' property value is required");
|
||||
Assert.notNull(systemProcessExitCodeMapper, "SystemProcessExitCodeMapper must be set");
|
||||
Assert.isTrue(timeout > 0, "timeout value must be greater than zero");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param systemProcessExitCodeMapper maps system process return value to
|
||||
* <code>ExitStatus</code> returned by Tasklet.
|
||||
* {@link SimpleSystemProcessExitCodeMapper} is used by default.
|
||||
*/
|
||||
public void setSystemProcessExitCodeMapper(SystemProcessExitCodeMapper systemProcessExitCodeMapper) {
|
||||
this.systemProcessExitCodeMapper = systemProcessExitCodeMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param timeout upper limit for how long the execution of the external
|
||||
* program is allowed to last.
|
||||
*/
|
||||
public void setTimeout(long timeout) {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Thread that executes the system command.
|
||||
*/
|
||||
private class ExecutorThread extends Thread {
|
||||
volatile int exitCode = -1;
|
||||
|
||||
volatile boolean finishedSuccessfully = false;
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
Process process = Runtime.getRuntime().exec(command, environmentParams, workingDirectory);
|
||||
exitCode = process.waitFor();
|
||||
finishedSuccessfully = true;
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new SystemCommandException("IO error while executing system command", e);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
throw new SystemCommandException("Interrupted while executing system command", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.springframework.batch.sample.tasklet;
|
||||
|
||||
import org.springframework.batch.repeat.ExitStatus;
|
||||
|
||||
/**
|
||||
* Maps the exit code of a system process to ExitStatus value
|
||||
* returned by a system command. Designed for use with the
|
||||
* {@link SystemCommandTasklet}.
|
||||
*
|
||||
* @author Robert Kasanicky
|
||||
*/
|
||||
public interface SystemProcessExitCodeMapper {
|
||||
|
||||
/**
|
||||
* @param exitCode exit code returned by the system process
|
||||
* @return ExitStatus appropriate for the <code>systemExitCode</code> parameter value
|
||||
*/
|
||||
ExitStatus getExitStatus(int exitCode);
|
||||
}
|
||||
@@ -14,20 +14,35 @@
|
||||
is the kind of task that is not natural to split into read and
|
||||
write. In this case the step is wrapped in a standalone job,
|
||||
however typically it would be a setup step in a multi-step job.
|
||||
|
||||
The second step illustrates executing a system command.
|
||||
</description>
|
||||
|
||||
<bean id="taskletJob" parent="simpleJob">
|
||||
<property name="steps">
|
||||
<bean id="deleteFilesInDir" parent="taskletStep">
|
||||
<property name="tasklet">
|
||||
<bean
|
||||
class="org.springframework.batch.sample.tasklet.FileDeletingTasklet">
|
||||
<property name="directoryResource"
|
||||
ref="directory" />
|
||||
</bean>
|
||||
</property>
|
||||
</bean>
|
||||
<list>
|
||||
<bean id="deleteFilesInDir" parent="taskletStep">
|
||||
<property name="tasklet">
|
||||
<bean
|
||||
class="org.springframework.batch.sample.tasklet.FileDeletingTasklet">
|
||||
<property name="directoryResource"
|
||||
ref="directory" />
|
||||
</bean>
|
||||
</property>
|
||||
</bean>
|
||||
<bean id="executeSystemCommand" parent="taskletStep">
|
||||
<property name="tasklet">
|
||||
<bean
|
||||
class="org.springframework.batch.sample.tasklet.SystemCommandTasklet">
|
||||
<property name="command" value="echo hello" />
|
||||
<!-- 5 second timeout for the command to complete -->
|
||||
<property name="timeout" value="5000" />
|
||||
</bean>
|
||||
</property>
|
||||
</bean>
|
||||
</list>
|
||||
</property>
|
||||
|
||||
</bean>
|
||||
|
||||
<bean id="directory"
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package org.springframework.batch.sample.tasklet;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.springframework.batch.repeat.ExitStatus;
|
||||
import org.springframework.batch.sample.tasklet.ConfigurableSystemProcessExitCodeMapper;
|
||||
|
||||
/**
|
||||
* Tests for {@link ConfigurableSystemProcessExitCodeMapper}
|
||||
*/
|
||||
public class ConfigurableSystemProcessExitCodeMapperTests extends TestCase {
|
||||
|
||||
private ConfigurableSystemProcessExitCodeMapper mapper = new ConfigurableSystemProcessExitCodeMapper();
|
||||
|
||||
/**
|
||||
* Regular usage scenario - mapping adheres to injected values
|
||||
*/
|
||||
public void testMapping() {
|
||||
Map mappings = new HashMap() {{
|
||||
put(new Integer(0), ExitStatus.FINISHED);
|
||||
put(new Integer(1), ExitStatus.FAILED);
|
||||
put(new Integer(2), ExitStatus.CONTINUABLE);
|
||||
put(new Integer(3), ExitStatus.NOOP);
|
||||
put(new Integer(4), ExitStatus.UNKNOWN);
|
||||
put(ConfigurableSystemProcessExitCodeMapper.ELSE_KEY, ExitStatus.UNKNOWN);
|
||||
}};
|
||||
|
||||
mapper.setMappings(mappings);
|
||||
|
||||
//check explicitly defined values
|
||||
for (Iterator iterator = mappings.entrySet().iterator(); iterator.hasNext();) {
|
||||
Map.Entry entry = (Map.Entry) iterator.next();
|
||||
if (entry.getKey().equals(ConfigurableSystemProcessExitCodeMapper.ELSE_KEY)) continue;
|
||||
|
||||
int exitCode = ((Integer)entry.getKey()).intValue();
|
||||
assertSame(entry.getValue(), mapper.getExitStatus(exitCode));
|
||||
}
|
||||
|
||||
//check the else clause
|
||||
assertSame(mappings.get(ConfigurableSystemProcessExitCodeMapper.ELSE_KEY),
|
||||
mapper.getExitStatus(5));
|
||||
}
|
||||
|
||||
/**
|
||||
* Else clause is required in the injected map - setter checks its presence.
|
||||
*/
|
||||
public void testSetMappingsMissingElseClause() {
|
||||
Map missingElse = Collections.EMPTY_MAP;
|
||||
try {
|
||||
mapper.setMappings(missingElse);
|
||||
fail();
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
// expected
|
||||
}
|
||||
|
||||
Map containsElse = new HashMap() {{
|
||||
put(ConfigurableSystemProcessExitCodeMapper.ELSE_KEY, ExitStatus.FAILED);
|
||||
}};
|
||||
// no error expected now
|
||||
mapper.setMappings(containsElse);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.springframework.batch.sample.tasklet;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.springframework.batch.repeat.ExitStatus;
|
||||
import org.springframework.batch.sample.tasklet.SimpleSystemProcessExitCodeMapper;
|
||||
|
||||
/**
|
||||
* Tests for {@link SimpleSystemProcessExitCodeMapper}.
|
||||
*/
|
||||
public class SimpleSystemProcessExitCodeMapperTests extends TestCase {
|
||||
|
||||
private SimpleSystemProcessExitCodeMapper mapper = new SimpleSystemProcessExitCodeMapper();
|
||||
|
||||
/**
|
||||
* 0 -> ExitStatus.FINISHED
|
||||
* else -> ExitStatus.FAILED
|
||||
*/
|
||||
public void testMapping() {
|
||||
assertEquals(ExitStatus.FINISHED, mapper.getExitStatus(0));
|
||||
assertEquals(ExitStatus.FAILED, mapper.getExitStatus(1));
|
||||
assertEquals(ExitStatus.FAILED, mapper.getExitStatus(-1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package org.springframework.batch.sample.tasklet;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.batch.repeat.ExitStatus;
|
||||
import org.springframework.batch.sample.tasklet.SystemCommandException;
|
||||
import org.springframework.batch.sample.tasklet.SystemCommandTasklet;
|
||||
import org.springframework.batch.sample.tasklet.SystemProcessExitCodeMapper;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Tests for {@link SystemCommandTasklet}.
|
||||
*/
|
||||
public class SystemCommandTaskletIntegrationTests extends TestCase {
|
||||
|
||||
private static final Log log = LogFactory.getLog(SystemCommandTaskletIntegrationTests.class);
|
||||
|
||||
private SystemCommandTasklet tasklet = new SystemCommandTasklet();
|
||||
|
||||
protected void setUp() throws Exception {
|
||||
tasklet.setEnvironmentParams(null); // inherit from parent process
|
||||
tasklet.setWorkingDirectory(null); // inherit from parent process
|
||||
tasklet.setSystemProcessExitCodeMapper(new TestExitCodeMapper());
|
||||
tasklet.setTimeout(5000); // long enough timeout
|
||||
}
|
||||
|
||||
/**
|
||||
* Regular usage scenario - successful execution of system command.
|
||||
*/
|
||||
public void testExecute() throws Exception {
|
||||
String command = "java -version";
|
||||
tasklet.setCommand(command);
|
||||
tasklet.afterPropertiesSet();
|
||||
|
||||
log.info("Executing command: " + command);
|
||||
ExitStatus exitStatus = tasklet.execute();
|
||||
|
||||
assertEquals(ExitStatus.FINISHED, exitStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Failed execution scenario - error exit code returned by system command.
|
||||
*/
|
||||
public void testExecuteFailure() throws Exception {
|
||||
String command = "java -invalid-argument";
|
||||
tasklet.setCommand(command);
|
||||
tasklet.afterPropertiesSet();
|
||||
|
||||
log.info("Executing command: " + command);
|
||||
ExitStatus exitStatus = tasklet.execute();
|
||||
|
||||
assertEquals(ExitStatus.FAILED, exitStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Failed execution scenario - execution time exceeds timeout.
|
||||
*/
|
||||
public void testExecuteTimeout() throws Exception {
|
||||
String command = "sleep 3";
|
||||
tasklet.setCommand(command);
|
||||
tasklet.setTimeout(10);
|
||||
tasklet.afterPropertiesSet();
|
||||
|
||||
log.info("Executing command: " + command);
|
||||
try {
|
||||
tasklet.execute();
|
||||
fail();
|
||||
}
|
||||
catch (SystemCommandException e) {
|
||||
assertTrue(e.getMessage().contains("did not finish successfully within the timeout"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Command property value is required to be set.
|
||||
*/
|
||||
public void testCommandNotSet() throws Exception {
|
||||
tasklet.setCommand(null);
|
||||
try {
|
||||
tasklet.afterPropertiesSet();
|
||||
fail();
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
// expected
|
||||
}
|
||||
|
||||
tasklet.setCommand("");
|
||||
try {
|
||||
tasklet.afterPropertiesSet();
|
||||
fail();
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Timeout must be set to non-zero value.
|
||||
*/
|
||||
public void testTimeoutNotSet() throws Exception {
|
||||
tasklet.setCommand("not-empty placeholder");
|
||||
tasklet.setTimeout(0);
|
||||
try {
|
||||
tasklet.afterPropertiesSet();
|
||||
fail();
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Working directory property must point to an existing location and it must
|
||||
* be a directory
|
||||
*/
|
||||
public void testWorkingDirectory() throws Exception {
|
||||
File notExistingFile = new File("not-existing-path");
|
||||
Assert.state(!notExistingFile.exists());
|
||||
|
||||
try {
|
||||
tasklet.setWorkingDirectory(notExistingFile.getCanonicalPath());
|
||||
fail();
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
// expected
|
||||
}
|
||||
|
||||
File notDirectory = File.createTempFile(this.getClass().getSimpleName(), null);
|
||||
Assert.state(notDirectory.exists());
|
||||
Assert.state(!notDirectory.isDirectory());
|
||||
|
||||
try {
|
||||
tasklet.setWorkingDirectory(notDirectory.getCanonicalPath());
|
||||
fail();
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
// expected
|
||||
}
|
||||
|
||||
File directory = notDirectory.getParentFile();
|
||||
Assert.state(directory.exists());
|
||||
Assert.state(directory.isDirectory());
|
||||
|
||||
// no error expected now
|
||||
tasklet.setWorkingDirectory(directory.getCanonicalPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Exit code mapper containing mapping logic expected by the tests. 0 means
|
||||
* finished successfully, other value means failure.
|
||||
*/
|
||||
private static class TestExitCodeMapper implements SystemProcessExitCodeMapper {
|
||||
|
||||
public ExitStatus getExitStatus(int exitCode) {
|
||||
if (exitCode == 0) {
|
||||
return ExitStatus.FINISHED;
|
||||
} else {
|
||||
return ExitStatus.FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user