org.rapidcontext.app.plugin.cmdline.CmdLineExecProcedure.java Source code

Java tutorial

Introduction

Here is the source code for org.rapidcontext.app.plugin.cmdline.CmdLineExecProcedure.java

Source

/**
 * RapidContext command-line plug-in <http://www.rapidcontext.com/>
 * Copyright (c) 2008-2013 Per Cederberg. All rights reserved.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the BSD license.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * See the RapidContext LICENSE for more details.
 */

package org.rapidcontext.app.plugin.cmdline;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang.StringUtils;

import org.rapidcontext.app.ApplicationContext;
import org.rapidcontext.core.data.Dict;
import org.rapidcontext.core.proc.AddOnProcedure;
import org.rapidcontext.core.proc.Bindings;
import org.rapidcontext.core.proc.CallContext;
import org.rapidcontext.core.proc.ProcedureException;

/**
 * A command-line execution procedure. This procedure provides the
 * functionality of executing local command-line programs and
 * capturing their output.
 *
 * @author   Per Cederberg
 * @version  1.0
 */
public class CmdLineExecProcedure extends AddOnProcedure {

    /**
     * The class logger.
     */
    private static final Logger LOG = Logger.getLogger(CmdLineExecProcedure.class.getName());

    /**
     * The binding name for the command to execute.
     */
    public static final String BINDING_COMMAND = "command";

    /**
     * The binding name for the working directory to use.
     */
    public static final String BINDING_DIRECTORY = "directory";

    /**
     * The binding name for the environment settings to use.
     */
    public static final String BINDING_ENVIRONMENT = "environment";

    /**
     * The progress information regular expression string.
     */
    private static final String PROGRESS_RE = "progress: (\\d+(\\.\\d+)?)%";

    /**
     * The progress information regular expression pattern.
     */
    private static final Pattern PROGRESS_PATTERN = Pattern.compile(PROGRESS_RE, Pattern.CASE_INSENSITIVE);

    /**
     * Creates a new command-line execution procedure.
     *
     * @throws ProcedureException if the initialization failed
     */
    public CmdLineExecProcedure() throws ProcedureException {
        defaults.set(BINDING_COMMAND, Bindings.DATA, "", "The command-line to execute.");
        defaults.set(BINDING_DIRECTORY, Bindings.DATA, "", "The working directory or blank for current.");
        defaults.set(BINDING_ENVIRONMENT, Bindings.DATA, "",
                "The environment variable bindings or blank for current.");
        defaults.seal();
    }

    /**
     * Executes a call of this procedure in the specified context
     * and with the specified call bindings. The semantics of what
     * the procedure actually does, is up to each implementation.
     * Note that the call bindings are normally inherited from the
     * procedure bindings with arguments bound to their call values.
     *
     * @param cx             the procedure call context
     * @param bindings       the call bindings to use
     *
     * @return the result of the call, or
     *         null if the call produced no result
     *
     * @throws ProcedureException if the call execution caused an
     *             error
     */
    public Object call(CallContext cx, Bindings bindings) throws ProcedureException {

        return execCall(cx, bindings);
    }

    /**
     * Executes a call of this procedure in the specified context
     * and with the specified call bindings.
     *
     * @param cx             the procedure call context
     * @param bindings       the call bindings to use
     *
     * @return the result of the call, or
     *         null if the call produced no result
     *
     * @throws ProcedureException if the call execution caused an
     *             error
     */
    static Object execCall(CallContext cx, Bindings bindings) throws ProcedureException {

        File dir = ApplicationContext.getInstance().getBaseDir();
        String str = bindings.getValue(BINDING_COMMAND).toString();
        // TODO: parse command-line properly, avoid StringTokenizer
        String cmd = replaceArguments(str, bindings).trim();
        str = ((String) bindings.getValue(BINDING_DIRECTORY, "")).trim();
        if (str.length() > 0) {
            str = replaceArguments(str, bindings);
            if (str.startsWith("/")) {
                dir = new File(str);
            } else {
                dir = new File(dir, str);
            }
        }
        String[] env = null;
        str = ((String) bindings.getValue(BINDING_ENVIRONMENT, "")).trim();
        if (str.length() > 0) {
            env = replaceArguments(str, bindings).split(";");
        }
        Runtime runtime = Runtime.getRuntime();
        LOG.fine("init exec: " + cmd);
        cx.log("Command: " + cmd);
        cx.log("Directory: " + dir);
        if (env != null) {
            cx.log("Environment: " + Arrays.toString(env));
        }
        try {
            // TODO: investigate using ProcessBuilder instead
            Process process = runtime.exec(cmd, env, dir);
            return waitFor(process, cx);
        } catch (IOException e) {
            str = "error executing '" + cmd + "': " + e.getMessage();
            LOG.log(Level.WARNING, str, e);
            throw new ProcedureException(str);
        } finally {
            LOG.fine("done exec: " + cmd);
        }
    }

    /**
     * Replaces any parameters with the corresponding argument value
     * from the bindings.
     *
     * @param data           the data string to process
     * @param bindings       the bindings to use
     *
     * @return the processed data string
     *
     * @throws ProcedureException if some parameter couldn't be found
     */
    private static String replaceArguments(String data, Bindings bindings) throws ProcedureException {

        String[] names = bindings.getNames();
        for (int i = 0; i < names.length; i++) {
            if (bindings.getType(names[i]) == Bindings.ARGUMENT) {
                Object value = bindings.getValue(names[i], null);
                if (value == null) {
                    value = "";
                }
                data = StringUtils.replace(data, ":" + names[i], value.toString());
            }
        }
        return data;
    }

    /**
     * Waits for the specified process to terminate, reading its
     * standard output and error streams meanwhile.
     *
     * @param process        the process to monitor
     * @param cx             the procedure call context
     *
     * @return the data object with the process output results
     *
     * @throws IOException if the stream reading failed
     */
    private static Dict waitFor(Process process, CallContext cx) throws IOException {

        StringBuffer output = new StringBuffer();
        StringBuffer error = new StringBuffer();
        InputStream isOut = process.getInputStream();
        InputStream isErr = process.getErrorStream();
        byte[] buffer = new byte[4096];
        int exitValue = 0;
        process.getOutputStream().close();
        while (true) {
            if (cx.isInterrupted()) {
                // TODO: isOut.close();
                // TODO: isErr.close();
                process.destroy();
                throw new IOException("procedure call interrupted");
            }
            try {
                readStream(isOut, buffer, output);
                readStream(isErr, buffer, error);
                log(cx, error);
                exitValue = process.exitValue();
                break;
            } catch (IllegalThreadStateException e) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException ignore) {
                    // Ignore this exception
                }
            }
        }
        Dict res = new Dict();
        res.setInt("exitValue", exitValue);
        res.set("output", output);
        return res;
    }

    /**
     * Reads all available bytes off an input stream.
     *
     * @param is             the input stream to read
     * @param buffer         the temporary read buffer
     * @param result         the result buffer to append to
     *
     * @throws IOException if the reading failed
     */
    private static void readStream(InputStream is, byte[] buffer, StringBuffer result) throws IOException {

        int avail = is.available();
        while (avail > 0) {
            if (avail > buffer.length) {
                avail = buffer.length;
            }
            is.read(buffer, 0, avail);
            result.append(new String(buffer, 0, avail));
            avail = is.available();
        }
    }

    /**
     * Analyzes the error output buffer from a process and adds the
     * relevant log messages
     *
     * @param cx             the procedure call context
     * @param buffer         the process error output buffer
     */
    private static void log(CallContext cx, StringBuffer buffer) {
        int pos;
        while ((pos = buffer.indexOf("\n")) >= 0) {
            String text = buffer.substring(0, pos).trim();
            if (text.length() > 0 && text.charAt(0) == '#') {
                if (cx.getCallStack().height() <= 1) {
                    Matcher m = PROGRESS_PATTERN.matcher(text);
                    if (m.find()) {
                        double progress = Double.parseDouble(m.group(1));
                        cx.setAttribute(CallContext.ATTRIBUTE_PROGRESS, Double.valueOf(progress));
                    }
                }
            } else {
                cx.log(buffer.substring(0, pos));
            }
            buffer.delete(0, pos + 1);
        }
    }
}