/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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.apache.tools.ant.taskdefs;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Vector;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.ExitException;
import org.apache.tools.ant.ExitStatusException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.condition.Os;
import org.apache.tools.ant.types.Assertions;
import org.apache.tools.ant.types.Commandline;
import org.apache.tools.ant.types.CommandlineJava;
import org.apache.tools.ant.types.Environment;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Permissions;
import org.apache.tools.ant.types.PropertySet;
import org.apache.tools.ant.types.RedirectorElement;
import org.apache.tools.ant.types.Reference;
import org.apache.tools.ant.util.KeepAliveInputStream;
import org.apache.tools.ant.util.StringUtils;

/**
 * Launcher for Java applications. Allows use of
 * the same JVM for the called application thus resulting in much
 * faster operation.
 *
 * @since Ant 1.1
 *
 * @ant.task category="java"
 */
public class Java extends Task {

    private CommandlineJava cmdl = new CommandlineJava();
    private Environment env = new Environment();
    private boolean fork = false;
    private boolean newEnvironment = false;
    private File dir = null;
    private boolean failOnError = false;
    private Long timeout = null;

    //include locally for screening purposes
    private String inputString;
    private File input;
    private File output;
    private File error;

    // CheckStyle:VisibilityModifier OFF - bc
    protected Redirector redirector = new Redirector(this);
    protected RedirectorElement redirectorElement;
    // CheckStyle:VisibilityModifier ON

    private String resultProperty;
    private Permissions perm = null;

    private boolean spawn = false;
    private boolean incompatibleWithSpawn = false;

    private static final String TIMEOUT_MESSAGE =
        "Timeout: killed the sub-process";

    /**
     * Normal constructor
     */
    public Java() {
    }

    /**
     * create a bound task
     * @param owner owner
     */
    public Java(Task owner) {
        bindToOwner(owner);
    }

    /**
     * Do the execution.
     * @throws BuildException if failOnError is set to true and the application
     * returns a nonzero result code.
     */
    public void execute() throws BuildException {
        File savedDir = dir;
        Permissions savedPermissions = perm;

        int err = -1;
        try {
            checkConfiguration();
            err = executeJava();
            if (err != 0) {
                if (failOnError) {
                    throw new ExitStatusException("Java returned: " + err,
                            err,
                            getLocation());
                } else {
                    log("Java Result: " + err, Project.MSG_ERR);
                }
            }
            maybeSetResultPropertyValue(err);
        } finally {
            dir = savedDir;
            perm = savedPermissions;
        }
    }

    /**
     * Do the execution and return a return code.
     *
     * @return the return code from the execute java class if it was
     * executed in a separate VM (fork = "yes") or a security manager was
     * installed that prohibits ExitVM (default).
     *
     * @throws BuildException if required parameters are missing.
     */
    public int executeJava() throws BuildException {
        return executeJava(getCommandLine());
    }

    /**
     * Check configuration.
     * @throws BuildException if required parameters are missing.
     */
    protected void checkConfiguration() throws BuildException {
        String classname = getCommandLine().getClassname();
        String module = getCommandLine().getModule();
        if (classname == null && getCommandLine().getJar() == null && module == null) {
            throw new BuildException("Classname must not be null.");
        }
        if (!fork && getCommandLine().getJar() != null) {
            throw new BuildException("Cannot execute a jar in non-forked mode."
                                     + " Please set fork='true'. ");
        }
        if (!fork && getCommandLine().getModule() != null) {
            throw new BuildException("Cannot execute a module in non-forked mode."
                                     + " Please set fork='true'. ");
        }
        if (spawn && !fork) {
            throw new BuildException("Cannot spawn a java process in non-forked mode."
                                     + " Please set fork='true'. ");
        }
        if (getCommandLine().getClasspath() != null
            && getCommandLine().getJar() != null) {
            log("When using 'jar' attribute classpath-settings are ignored. "
                + "See the manual for more information.", Project.MSG_VERBOSE);
        }
        if (spawn && incompatibleWithSpawn) {
            getProject().log("spawn does not allow attributes related to input, "
            + "output, error, result", Project.MSG_ERR);
            getProject().log("spawn also does not allow timeout", Project.MSG_ERR);
            getProject().log("finally, spawn is not compatible "
                + "with a nested I/O <redirector>", Project.MSG_ERR);
            throw new BuildException("You have used an attribute "
                + "or nested element which is not compatible with spawn");
        }
        if (getCommandLine().getAssertions() != null && !fork) {
            log("Assertion statements are currently ignored in non-forked mode");
        }
        if (fork) {
            if (perm != null) {
                log("Permissions can not be set this way in forked mode.", Project.MSG_WARN);
            }
            log(getCommandLine().describeCommand(), Project.MSG_VERBOSE);
        } else {
            if (getCommandLine().getVmCommand().size() > 1) {
                log("JVM args ignored when same JVM is used.",
                    Project.MSG_WARN);
            }
            if (dir != null) {
                log("Working directory ignored when same JVM is used.",
                    Project.MSG_WARN);
            }
            if (newEnvironment || null != env.getVariables()) {
                log("Changes to environment variables are ignored when same "
                    + "JVM is used.", Project.MSG_WARN);
            }
            if (getCommandLine().getBootclasspath() != null) {
                log("bootclasspath ignored when same JVM is used.",
                    Project.MSG_WARN);
            }
            if (perm == null) {
                perm = new Permissions(true);
                log("running " + this.getCommandLine().getClassname()
                    + " with default permissions (exit forbidden)", Project.MSG_VERBOSE);
            }
            log("Running in same VM " + getCommandLine().describeJavaCommand(),
                Project.MSG_VERBOSE);
        }
        setupRedirector();
    }

    /**
     * Execute the specified CommandlineJava.
     * @param commandLine CommandLineJava instance.
     * @return the exit value of the process if forked, 0 otherwise.
     */
    protected int executeJava(CommandlineJava commandLine) {
        try {
            if (fork) {
                if (!spawn) {
                    return fork(commandLine.getCommandline());
                } else {
                    spawn(commandLine.getCommandline());
                    return 0;
                }
            } else {
                try {
                    run(commandLine);
                    return 0;
                } catch (ExitException ex) {
                    return ex.getStatus();
                }
            }
        } catch (BuildException e) {
            if (e.getLocation() == null && getLocation() != null) {
                e.setLocation(getLocation());
            }
            if (failOnError) {
                throw e;
            } else {
                if (TIMEOUT_MESSAGE.equals(e.getMessage())) {
                    log(TIMEOUT_MESSAGE);
                } else {
                    log(e);
                }
                return -1;
            }
        } catch (ThreadDeath t) {
            throw t; // cf. NB #47191
        } catch (Throwable t) {
            if (failOnError) {
                throw new BuildException(t, getLocation());
            } else {
                log(t);
                return -1;
            }
        }
    }

    /**
     * Set whether or not you want the process to be spawned;
     * default is not spawned.
     * @param spawn if true you do not want Ant to wait for the end of the process.
     * @since Ant 1.6
     */
    public void setSpawn(boolean spawn) {
        this.spawn = spawn;
    }

    /**
     * Set the classpath to be used when running the Java class.
     *
     * @param s an Ant Path object containing the classpath.
     */
    public void setClasspath(Path s) {
        createClasspath().append(s);
    }

    /**
     * Add a path to the classpath.
     *
     * @return created classpath.
     */
    public Path createClasspath() {
        return getCommandLine().createClasspath(getProject()).createPath();
    }

    /**
     * Add a path to the bootclasspath.
     * @since Ant 1.6
     *
     * @return created bootclasspath.
     */
    public Path createBootclasspath() {
        return getCommandLine().createBootclasspath(getProject()).createPath();
    }

    /**
     * Set the modulepath to be used when running the Java class.
     *
     * @param mp an Ant Path object containing the modulepath.
     * @since 1.9.7
     */
    public void setModulepath(Path mp) {
        createModulepath().append(mp);
    }

    /**
     * Add a path to the modulepath.
     *
     * @return created modulepath.
     * @since 1.9.7
     */
    public Path createModulepath() {
        return getCommandLine().createModulepath(getProject()).createPath();
    }

    /**
     * Set the modulepath to use by reference.
     *
     * @param r a reference to an existing modulepath.
     * @since 1.9.7
     */
    public void setModulepathRef(Reference r) {
        createModulepath().setRefid(r);
    }

    /**
     * Add a path to the upgrademodulepath.
     *
     * @return created upgrademodulepath.
     * @since 1.9.7
     */
    public Path createUpgrademodulepath() {
        return getCommandLine().createUpgrademodulepath(getProject()).createPath();
    }

    /**
     * Set the permissions for the application run inside the same JVM.
     * @since Ant 1.6
     * @return Permissions.
     */
    public Permissions createPermissions() {
        perm = (perm == null) ? new Permissions() : perm;
        return perm;
    }

    /**
     * Set the classpath to use by reference.
     *
     * @param r a reference to an existing classpath.
     */
    public void setClasspathRef(Reference r) {
        createClasspath().setRefid(r);
    }

    /**
     * Set the location of the JAR file to execute.
     *
     * @param jarfile the jarfile to execute.
     *
     * @throws BuildException if there is also a main class specified.
     */
    public void setJar(File jarfile) throws BuildException {
        if (getCommandLine().getClassname() != null || getCommandLine().getModule() != null) {
            throw new BuildException("Cannot use 'jar' with 'classname' or 'module' "
                                     + "attributes in same command.");
        }
        getCommandLine().setJar(jarfile.getAbsolutePath());
    }

    /**
     * Set the Java class to execute.
     *
     * @param s the name of the main class.
     *
     * @throws BuildException if the jar attribute has been set.
     */
    public void setClassname(String s) throws BuildException {
        if (getCommandLine().getJar() != null) {
            throw new BuildException("Cannot use 'jar' and 'classname' "
                                     + "attributes in same command");
        }
        getCommandLine().setClassname(s);
    }

    /**
     * Set the Java module to execute.
     *
     * @param module the name of the module.
     *
     * @throws BuildException if the jar attribute has been set.
     * @since 1.9.7
     */
    public void setModule(String module) throws BuildException {
        if (getCommandLine().getJar() != null) {
            throw new BuildException("Cannot use 'jar' and 'module' "
                                     + "attributes in same command");
        }
        getCommandLine().setModule(module);
    }

    /**
     * Deprecated: use nested arg instead.
     * Set the command line arguments for the class.
     *
     * @param s arguments.
     *
     * @ant.attribute ignore="true"
     */
    public void setArgs(String s) {
        log("The args attribute is deprecated. "
            + "Please use nested arg elements.", Project.MSG_WARN);
        getCommandLine().createArgument().setLine(s);
    }

    /**
     * If set, system properties will be copied to the cloned VM--as
     * well as the bootclasspath unless you have explicitly specified
     * a bootclasspath.
     *
     * <p>Doesn't have any effect unless fork is true.</p>
     * @param cloneVm if true copy system properties.
     * @since Ant 1.7
     */
    public void setCloneVm(boolean cloneVm) {
        getCommandLine().setCloneVm(cloneVm);
    }

    /**
     * Add a command-line argument.
     *
     * @return created argument.
     */
    public Commandline.Argument createArg() {
        return getCommandLine().createArgument();
    }

    /**
     * Set the name of the property in which the return code of the
     * command should be stored. Only of interest if failonerror=false.
     *
     * @param resultProperty name of property.
     *
     * @since Ant 1.6
     */
    public void setResultProperty(String resultProperty) {
        this.resultProperty = resultProperty;
        incompatibleWithSpawn = true;
    }

    /**
     * Helper method to set result property to the
     * passed in value if appropriate.
     *
     * @param result the exit code
     */
    protected void maybeSetResultPropertyValue(int result) {
        String res = Integer.toString(result);
        if (resultProperty != null) {
            getProject().setNewProperty(resultProperty, res);
        }
    }

    /**
     * If true, execute in a new VM.
     *
     * @param s do you want to run Java in a new VM.
     */
    public void setFork(boolean s) {
        this.fork = s;
    }

    /**
     * Set the command line arguments for the JVM.
     *
     * @param s jvmargs.
     */
    public void setJvmargs(String s) {
        log("The jvmargs attribute is deprecated. "
            + "Please use nested jvmarg elements.", Project.MSG_WARN);
        getCommandLine().createVmArgument().setLine(s);
    }

    /**
     * Adds a JVM argument.
     *
     * @return JVM argument created.
     */
    public Commandline.Argument createJvmarg() {
        return getCommandLine().createVmArgument();
    }

    /**
     * Set the command used to start the VM (only if forking).
     *
     * @param s command to start the VM.
     */
    public void setJvm(String s) {
        getCommandLine().setVm(s);
    }

    /**
     * Add a system property.
     *
     * @param sysp system property.
     */
    public void addSysproperty(Environment.Variable sysp) {
        getCommandLine().addSysproperty(sysp);
    }

    /**
     * Add a set of properties as system properties.
     *
     * @param sysp set of properties to add.
     *
     * @since Ant 1.6
     */
    public void addSyspropertyset(PropertySet sysp) {
        getCommandLine().addSyspropertyset(sysp);
    }

    /**
     * If true, then fail if the command exits with a
     * returncode other than zero.
     *
     * @param fail if true fail the build when the command exits with a
     * nonzero returncode.
     */
    public void setFailonerror(boolean fail) {
        failOnError = fail;
        incompatibleWithSpawn |= fail;
    }

    /**
     * Set the working directory of the process.
     *
     * @param d working directory.
     *
     */
    public void setDir(File d) {
        this.dir = d;
    }

    /**
     * Set the File to which the output of the process is redirected.
     *
     * @param out the output File.
     */
    public void setOutput(File out) {
        this.output = out;
        incompatibleWithSpawn = true;
    }

    /**
     * Set the input to use for the task.
     *
     * @param input name of the input file.
     */
    public void setInput(File input) {
        if (inputString != null) {
            throw new BuildException("The \"input\" and \"inputstring\" "
                + "attributes cannot both be specified");
        }
        this.input = input;
        incompatibleWithSpawn = true;
    }

    /**
     * Set the string to use as input.
     *
     * @param inputString the string which is used as the input source.
     */
    public void setInputString(String inputString) {
        if (input != null) {
            throw new BuildException("The \"input\" and \"inputstring\" "
                + "attributes cannot both be specified");
        }
        this.inputString = inputString;
        incompatibleWithSpawn = true;
    }

    /**
     * Set whether error output of exec is logged. This is only useful
     * when output is being redirected and error output is desired in the
     * Ant log.
     *
     * @param logError get in the ant log the messages coming from stderr
     * in the case that fork = true.
     */
    public void setLogError(boolean logError) {
        redirector.setLogError(logError);
        incompatibleWithSpawn |= logError;
    }

    /**
     * Set the File to which the error stream of the process is redirected.
     *
     * @param error file getting the error stream.
     *
     * @since Ant 1.6
     */
    public void setError(File error) {
        this.error = error;
        incompatibleWithSpawn = true;
    }

    /**
     * Set the property name whose value should be set to the output of
     * the process.
     *
     * @param outputProp property name.
     *
     */
    public void setOutputproperty(String outputProp) {
        redirector.setOutputProperty(outputProp);
        incompatibleWithSpawn = true;
    }

    /**
     * Set the property name whose value should be set to the error of
     * the process.
     *
     * @param errorProperty property name.
     *
     * @since Ant 1.6
     */
    public void setErrorProperty(String errorProperty) {
        redirector.setErrorProperty(errorProperty);
        incompatibleWithSpawn = true;
    }

    /**
     * Corresponds to -mx or -Xmx depending on VM version.
     *
     * @param max max memory parameter.
     */
    public void setMaxmemory(String max) {
        getCommandLine().setMaxmemory(max);
    }

    /**
     * Set the JVM version.
     * @param value JVM version.
     */
    public void setJVMVersion(String value) {
        getCommandLine().setVmversion(value);
    }

    /**
     * Add an environment variable.
     *
     * <p>Will be ignored if we are not forking a new VM.
     *
     * @param var new environment variable.
     *
     * @since Ant 1.5
     */
    public void addEnv(Environment.Variable var) {
        env.addVariable(var);
    }

    /**
     * If true, use a completely new environment.
     *
     * <p>Will be ignored if we are not forking a new VM.
     *
     * @param newenv if true, use a completely new environment.
     *
     * @since Ant 1.5
     */
    public void setNewenvironment(boolean newenv) {
        newEnvironment = newenv;
    }

    /**
     * If true, append output to existing file.
     *
     * @param append if true, append output to existing file.
     *
     * @since Ant 1.5
     */
    public void setAppend(boolean append) {
        redirector.setAppend(append);
        incompatibleWithSpawn |= append;
    }

    /**
     * Set the timeout in milliseconds after which the process will be killed.
     *
     * @param value timeout in milliseconds.
     *
     * @since Ant 1.5
     */
    public void setTimeout(Long value) {
        timeout = value;
        incompatibleWithSpawn |= timeout != null;
    }

    /**
     * Add assertions to enable in this program (if fork=true).
     * @param asserts assertion set.
     * @since Ant 1.6
     */
    public void addAssertions(Assertions asserts) {
        if (getCommandLine().getAssertions() != null) {
            throw new BuildException("Only one assertion declaration is allowed");
        }
        getCommandLine().setAssertions(asserts);
    }

    /**
     * Add a <code>RedirectorElement</code> to this task.
     * @param redirectorElement   <code>RedirectorElement</code>.
     */
    public void addConfiguredRedirector(RedirectorElement redirectorElement) {
        if (this.redirectorElement != null) {
            throw new BuildException("cannot have > 1 nested redirectors");
        }
        this.redirectorElement = redirectorElement;
        incompatibleWithSpawn = true;
    }

    /**
     * Pass output sent to System.out to specified output file.
     *
     * @param output a string of output on its way to the handlers.
     *
     * @since Ant 1.5
     */
    protected void handleOutput(String output) {
        if (redirector.getOutputStream() != null) {
            redirector.handleOutput(output);
        } else {
            super.handleOutput(output);
        }
    }

    /**
     * Handle an input request by this task.
     *
     * @param buffer the buffer into which data is to be read.
     * @param offset the offset into the buffer at which data is stored.
     * @param length the amount of data to read.
     *
     * @return the number of bytes read.
     *
     * @exception IOException if the data cannot be read.
     * @since Ant 1.6
     */
    public int handleInput(byte[] buffer, int offset, int length)
        throws IOException {
        // Should work whether or not redirector.inputStream == null:
        return redirector.handleInput(buffer, offset, length);
    }

    /**
     * Pass output sent to System.out to specified output file.
     *
     * @param output string of output on its way to its handlers.
     *
     * @since Ant 1.5.2
     */
    protected void handleFlush(String output) {
        if (redirector.getOutputStream() != null) {
            redirector.handleFlush(output);
        } else {
            super.handleFlush(output);
        }
    }

    /**
     * Handle output sent to System.err.
     *
     * @param output string of stderr.
     *
     * @since Ant 1.5
     */
    protected void handleErrorOutput(String output) {
        if (redirector.getErrorStream() != null) {
            redirector.handleErrorOutput(output);
        } else {
            super.handleErrorOutput(output);
        }
    }

    /**
     * Handle output sent to System.err and flush the stream.
     *
     * @param output string of stderr.
     *
     * @since Ant 1.5.2
     */
    protected void handleErrorFlush(String output) {
        if (redirector.getErrorStream() != null) {
            redirector.handleErrorFlush(output);
        } else {
            super.handleErrorFlush(output);
        }
    }

    /**
     * Set up properties on the redirector that we needed to store locally.
     */
    protected void setupRedirector() {
        redirector.setInput(input);
        redirector.setInputString(inputString);
        redirector.setOutput(output);
        redirector.setError(error);
        if (redirectorElement != null) {
            redirectorElement.configure(redirector);
        }
        if (!spawn && input == null && inputString == null) {
            // #24918: send standard input to the process by default.
            redirector.setInputStream(
                new KeepAliveInputStream(getProject().getDefaultInputStream()));
        }
    }

    /**
     * Executes the given classname with the given arguments as it
     * were a command line application.
     * @param command CommandlineJava.
     */
    private void run(CommandlineJava command) throws BuildException {
        try {
            ExecuteJava exe = new ExecuteJava();
            exe.setJavaCommand(command.getJavaCommand());
            exe.setClasspath(command.getClasspath());
            exe.setSystemProperties(command.getSystemProperties());
            exe.setPermissions(perm);
            exe.setTimeout(timeout);
            redirector.createStreams();
            exe.execute(getProject());
            redirector.complete();
            if (exe.killedProcess()) {
                throw new BuildException(TIMEOUT_MESSAGE);
            }
        } catch (IOException e) {
            throw new BuildException(e);
        }
    }

    /**
     * Executes the given classname with the given arguments in a separate VM.
     * @param command String[] of command-line arguments.
     */
    private int fork(String[] command) throws BuildException {
        Execute exe
            = new Execute(redirector.createHandler(), createWatchdog());
        setupExecutable(exe, command);

        try {
            int rc = exe.execute();
            redirector.complete();
            if (exe.killedProcess()) {
                throw new BuildException(TIMEOUT_MESSAGE);
            }
            return rc;
        } catch (IOException e) {
            throw new BuildException(e, getLocation());
        }
    }

    /**
     * Executes the given classname with the given arguments in a separate VM.
     * @param command String[] of command-line arguments.
     */
    private void spawn(String[] command) throws BuildException {
        Execute exe = new Execute();
        setupExecutable(exe, command);
        try {
            exe.spawn();
        } catch (IOException e) {
            throw new BuildException(e, getLocation());
        }
    }

    /**
     * Do all configuration for an executable that
     * is common across the {@link #fork(String[])} and
     * {@link #spawn(String[])} methods.
     * @param exe executable.
     * @param command command to execute.
     */
    private void setupExecutable(Execute exe, String[] command) {
        exe.setAntRun(getProject());
        setupWorkingDir(exe);
        setupEnvironment(exe);
        setupCommandLine(exe, command);
    }

    /**
     * Set up our environment variables.
     * @param exe executable.
     */
    private void setupEnvironment(Execute exe) {
        String[] environment = env.getVariables();
        if (environment != null) {
            for (int i = 0; i < environment.length; i++) {
                log("Setting environment variable: " + environment[i],
                    Project.MSG_VERBOSE);
            }
        }
        exe.setNewenvironment(newEnvironment);
        exe.setEnvironment(environment);
    }

    /**
     * Set the working dir of the new process.
     * @param exe executable.
     * @throws BuildException if the dir doesn't exist.
     */
    private void setupWorkingDir(Execute exe) {
        if (dir == null) {
            dir = getProject().getBaseDir();
        } else if (!dir.exists() || !dir.isDirectory()) {
            throw new BuildException(dir.getAbsolutePath()
                                     + " is not a valid directory",
                                     getLocation());
        }
        exe.setWorkingDirectory(dir);
    }

    /**
     * Set the command line for the exe.
     * On VMS, hands off to {@link #setupCommandLineForVMS(Execute, String[])}.
     * @param exe executable.
     * @param command command to execute.
     */
    private void setupCommandLine(Execute exe, String[] command) {
        //On VMS platform, we need to create a special java options file
        //containing the arguments and classpath for the java command.
        //The special file is supported by the "-V" switch on the VMS JVM.
        if (Os.isFamily("openvms")) {
            setupCommandLineForVMS(exe, command);
        } else {
            exe.setCommandline(command);
        }
    }

    /**
     * On VMS platform, we need to create a special java options file
     * containing the arguments and classpath for the java command.
     * The special file is supported by the "-V" switch on the VMS JVM.
     *
     * @param exe executable.
     * @param command command to execute.
     */
    private void setupCommandLineForVMS(Execute exe, String[] command) {
        ExecuteJava.setupCommandLineForVMS(exe, command);
    }

    /**
     * Executes the given classname with the given arguments as if it
     * were a command line application.
     *
     * @param classname the name of the class to run.
     * @param args  arguments for the class.
     * @throws BuildException in case of IOException in the execution.
     */
    protected void run(String classname, Vector<String> args) throws BuildException {
        CommandlineJava cmdj = new CommandlineJava();
        cmdj.setClassname(classname);
        final int size = args.size();
        for (int i = 0; i < size; i++) {
            cmdj.createArgument().setValue(args.elementAt(i));
        }
        run(cmdj);
    }

    /**
     * Clear out the arguments to this java task.
     */
    public void clearArgs() {
        getCommandLine().clearJavaArgs();
    }

    /**
     * Create the Watchdog to kill a runaway process.
     *
     * @return new watchdog.
     *
     * @throws BuildException under unknown circumstances.
     *
     * @since Ant 1.5
     */
    protected ExecuteWatchdog createWatchdog() throws BuildException {
        if (timeout == null) {
            return null;
        }
        return new ExecuteWatchdog(timeout.longValue());
    }

    /**
     * Log the specified Throwable.
     * @param t the Throwable to log.
     * @since 1.6.2
     */
    private void log(Throwable t) {
        log(StringUtils.getStackTrace(t), Project.MSG_ERR);
    }

    /**
     * Accessor to the command line.
     *
     * @return the current command line.
     * @since 1.6.3
     */
    public CommandlineJava getCommandLine() {
        return cmdl;
    }

    /**
     * Get the system properties of the command line.
     *
     * @return the current properties of this java invocation.
     * @since 1.6.3
     */
    public CommandlineJava.SysProperties getSysProperties() {
        return getCommandLine().getSystemProperties();
    }
}
