org.springframework.xd.extension.process.ShellCommandProcessor.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.xd.extension.process.ShellCommandProcessor.java

Source

package org.springframework.xd.extension.process;/*
                                                 *
                                                 *  * Copyright 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.
                                                 *
                                                 */

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.Lifecycle;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.TaskExecutor;
import org.springframework.integration.ip.tcp.serializer.AbstractByteArraySerializer;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

/**
 * Creates a process to run a shell command and communicate with it using String payloads over stdin and stdout.
 *
 * @author David Turanski
 */
public class ShellCommandProcessor implements Lifecycle, InitializingBean {
    private final AtomicBoolean running = new AtomicBoolean();

    private final ProcessBuilder processBuilder;

    private Process process;

    private InputStream stdout;

    private OutputStream stdin;

    private boolean redirectErrorStream;

    private Map<String, String> environment;

    private String workingDirectory;

    private String charset = "UTF-8";

    private final AbstractByteArraySerializer serializer;

    private final static Log log = LogFactory.getLog(ShellCommandProcessor.class);

    private TaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();

    private final String command;

    /**
     * Creates a process to invoke a shell command to send and receive messages from the processes using the process's stdin and stdout.
     *
     * @param serializer an {@link org.springframework.integration.ip.tcp.serializer.AbstractByteArraySerializer} to delimit messages
     * @param command the shell command with command line arguments as separate strings
     */
    public ShellCommandProcessor(AbstractByteArraySerializer serializer, String command) {
        Assert.hasLength(command, "A shell command is required");
        Assert.notNull(serializer, "'serializer' cannot be null");
        this.command = command;
        List<String> commandPlusArgs = parse(command);
        Assert.notEmpty(commandPlusArgs, "The shell command is invalid: '" + command + "'");
        this.serializer = serializer;
        processBuilder = new ProcessBuilder(commandPlusArgs);
    }

    /**
     * Start the process.
     */
    @Override
    public void start() {
        if (!isRunning()) {

            if (log.isDebugEnabled()) {
                log.debug("starting process. Command = [" + command + "]");
            }

            try {
                process = processBuilder.start();
            } catch (IOException e) {
                log.error(e.getMessage(), e);
                throw new RuntimeException(e.getMessage(), e);
            }

            if (!processBuilder.redirectErrorStream()) {
                monitorErrorStream();
            }
            monitorProcess();

            stdout = process.getInputStream();
            stdin = process.getOutputStream();

            running.set(true);
            if (log.isDebugEnabled()) {
                log.debug("process started. Command = [" + command + "]");
            }
        }
    }

    /**
     * Receive data from the process.
     * @return any available data from stdout
     */
    public String receive() {
        Assert.isTrue(isRunning(), "Shell process is not started.");
        String data;
        try {
            byte[] buffer = serializer.deserialize(stdout);
            data = new String(buffer, charset);
        } catch (IOException e) {
            throw new RuntimeException(e.getMessage(), e);
        }
        return data == null ? null : data.trim();
    }

    /**
     * Send data as a String to stdin.
     * @param data the data
     */
    public void send(String data) {
        try {
            serializer.serialize(data.getBytes(charset), stdin);
        } catch (IOException e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException(e.getMessage(), e);
        }
    }

    /**
     * Send and receive data in request/response fashion.
     * @param data the input
     * @return the output
     */
    public String sendAndReceive(String data) {
        Assert.isTrue(isRunning(), "Shell process is not started");
        send(data);
        return receive();
    }

    /**
     * Stop the process and close streams.
     */
    @Override
    public void stop() {
        if (isRunning()) {
            process.destroy();
            running.set(false);
        }
    }

    @Override
    public boolean isRunning() {
        return running.get();
    }

    /**
     * Set to true to redirect stderr to stdout.
     * @param redirectErrorStream
     */
    public void setRedirectErrorStream(boolean redirectErrorStream) {
        this.redirectErrorStream = redirectErrorStream;
    }

    /**
     * A map containing environment variables to add to the process environment.
     * @param environment
     */
    public void setEnvironment(Map<String, String> environment) {
        this.environment = environment;
    }

    /**
     * Set the process working directory
     * @param workingDirectory the file path
     */
    public void setWorkingDirectory(String workingDirectory) {
        this.workingDirectory = workingDirectory;
    }

    /**
     * Set the charset name for String encoding. Default is UTF-8
     * @param charset the charset name
     */
    public void setCharset(String charset) {
        this.charset = charset;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        processBuilder.redirectErrorStream(redirectErrorStream);

        if (StringUtils.hasLength(workingDirectory)) {
            processBuilder.directory(new File(workingDirectory));
        }
        if (!CollectionUtils.isEmpty(environment)) {
            processBuilder.environment().putAll(environment);
        }
    }

    /**
     * Handle extra white space between arguments
     */
    private List<String> parse(String command) {
        List<String> result = new ArrayList<String>();
        for (String token : StringUtils.delimitedListToStringArray(command, " ")) {
            if (token.trim().length() > 0) {
                result.add(token);
            }
        }
        return result;
    }

    /**
     * Runs a thread that waits for the Process result.
     */
    private void monitorProcess() {
        taskExecutor.execute(new Runnable() {

            @Override
            public void run() {
                Process process = ShellCommandProcessor.this.process;
                if (process == null) {
                    if (log.isDebugEnabled()) {
                        log.debug("Process destroyed before starting process monitor");
                    }
                    return;
                }

                int result = Integer.MIN_VALUE;
                try {
                    if (log.isDebugEnabled()) {
                        log.debug("Monitoring process '" + command + "'");
                    }
                    result = process.waitFor();
                    if (log.isInfoEnabled()) {
                        log.info("Process '" + command + "' terminated with value " + result);

                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    log.error("Interrupted - stopping adapter", e);
                    stop();
                } finally {
                    process.destroy();
                }
            }
        });
    }

    /**
     * Runs a thread that reads stderr
     */
    private void monitorErrorStream() {
        Process process = this.process;
        if (process == null) {
            if (log.isDebugEnabled()) {
                log.debug("Process destroyed before starting stderr reader");
            }
            return;
        }
        final BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
        taskExecutor.execute(new Runnable() {

            @Override
            public void run() {
                String statusMessage;
                if (log.isDebugEnabled()) {
                    log.debug("Reading stderr");
                }
                try {
                    while ((statusMessage = errorReader.readLine()) != null) {
                        log.error(statusMessage);
                    }
                } catch (IOException e) {
                    if (log.isDebugEnabled()) {
                        log.debug("Exception on process error reader", e);
                    }
                } finally {
                    try {
                        errorReader.close();
                    } catch (IOException e) {
                        if (log.isDebugEnabled()) {
                            log.debug("Exception while closing stderr", e);
                        }
                    }
                }
            }
        });
    }

}