Command.java

package gov.usgs.earthquake.distribution;

import gov.usgs.util.StreamUtils;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.Timer;
import java.util.TimerTask;

/** Class encapsulates commands including command line arguments */
public class Command {

  private String[] commandArray = null;
  private String[] envp = null;
  private File dir = null;

  private InputStream stdin = null;
  private ByteArrayOutputStream stdout = new ByteArrayOutputStream();
  private ByteArrayOutputStream stderr = new ByteArrayOutputStream();

  private long timeout = 0;
  private int exitCode = -1;

  /** Empty command constructor */
  public Command() {
  }

  /**
   * @throws CommandTimeout       CommandTimeout
   * @throws IOException          IOException
   * @throws InterruptedException InterruptedException
   */
  public void execute() throws CommandTimeout, IOException, InterruptedException {
    StreamTransferThread outputTransfer = null;
    StreamTransferThread errorTransfer = null;

    try {
      final Process process = Runtime.getRuntime().exec(commandArray, envp, dir);

      final Timer timer;
      if (timeout > 0) {
        timer = new Timer();
        timer.schedule(new TimerTask() {
          public void run() {
            process.destroy();
          }
        }, timeout);
      } else {
        timer = null;
      }

      try {
        OutputStream processStdin = process.getOutputStream();
        if (stdin != null) {
          StreamUtils.transferStream(stdin, processStdin);
        }
        StreamUtils.closeStream(processStdin);

        outputTransfer = new StreamTransferThread(process.getInputStream(), stdout);
        outputTransfer.start();
        errorTransfer = new StreamTransferThread(process.getErrorStream(), stderr);
        errorTransfer.start();

        // now wait for process to complete
        exitCode = process.waitFor();
        if (exitCode == 143) {
          throw new CommandTimeout();
        }
      } finally {
        // cancel destruction of process, if it hasn't already run
        if (timer != null) {
          timer.cancel();
        }
      }
    } finally {
      try {
        outputTransfer.interrupt();
        outputTransfer.join();
      } catch (Exception e) {
      }
      try {
        errorTransfer.interrupt();
        errorTransfer.join();
      } catch (Exception e) {
      }
    }
  }

  /** @return string[] */
  public String[] getCommand() {
    return commandArray;
  }

  /** @param command single command */
  public void setCommand(final String command) {
    setCommand(splitCommand(command));
  }

  /** @param commandArray string[] */
  public void setCommand(final String[] commandArray) {
    this.commandArray = commandArray;
  }

  /** @return envp */
  public String[] getEnvp() {
    return envp;
  }

  /** @param envp String[] */
  public void setEnvp(final String[] envp) {
    this.envp = envp;
  }

  /** @return dir */
  public File getDir() {
    return dir;
  }

  /** @param dir File */
  public void setDir(final File dir) {
    this.dir = dir;
  }

  /** @return timeout */
  public long getTimeout() {
    return timeout;
  }

  /** @param timeout long */
  public void setTimeout(final long timeout) {
    this.timeout = timeout;
  }

  /** @return exitCode */
  public int getExitCode() {
    return exitCode;
  }

  /** @param stdin InputStream */
  public void setStdin(final InputStream stdin) {
    this.stdin = stdin;
  }

  /** @return stdout byte[] */
  public byte[] getStdout() {
    return stdout.toByteArray();
  }

  /** @return stderr byte[] */
  public byte[] getStderr() {
    return stderr.toByteArray();
  }

  /**
   * Split a command string into a command array.
   *
   * This version uses a StringTokenizer to split arguments. Quoted arguments are
   * supported (single or double), with quotes removed before passing to runtime.
   * Double quoting arguments will preserve quotes when passing to runtime.
   *
   * @param command command to run.
   * @return Array of arguments suitable for passing to Runtime.exec(String[]).
   */
  protected static String[] splitCommand(final String command) {
    List<String> arguments = new LinkedList<String>();
    String currentArgument = null;

    // use a tokenizer because that's how Runtime.exec does it currently...
    StringTokenizer tokens = new StringTokenizer(command);
    while (tokens.hasMoreTokens()) {
      String token = tokens.nextToken();

      if (currentArgument == null) {
        currentArgument = token;
      } else {
        // continuing previous argument, that was split on whitespace
        currentArgument = currentArgument + " " + token;
      }

      if (currentArgument.startsWith("\"")) {
        // double quoted argument
        if (currentArgument.endsWith("\"")) {
          // that has balanced quotes
          // remove quotes and add argument
          currentArgument = currentArgument.substring(1, currentArgument.length() - 1);
        } else {
          // unbalanced quotes, keep going
          continue;
        }
      } else if (currentArgument.startsWith("'")) {
        // single quoted argument
        if (currentArgument.endsWith("'")) {
          // that has balanced quotes
          // remove quotes and add argument
          currentArgument = currentArgument.substring(1, currentArgument.length() - 1);
        } else {
          // unbalanced quotes, keep going
          continue;
        }
      }

      arguments.add(currentArgument);
      currentArgument = null;
    }

    if (currentArgument != null) {
      // weird, but add argument anyways
      arguments.add(currentArgument);
    }

    return arguments.toArray(new String[] {});
  }

  /** Static exception class for when a command times out */
  public static class CommandTimeout extends Exception {

    private static final long serialVersionUID = 1L;

  }

  /** Static thread class with input and output streams */
  private static class StreamTransferThread extends Thread {
    private InputStream in;
    private OutputStream out;

    public StreamTransferThread(final InputStream in, final OutputStream out) {
      this.in = in;
      this.out = out;
    }

    @Override
    public void run() {
      try {
        StreamUtils.transferStream(in, out);
      } catch (Exception e) {
      }
    }
  }

}