/*
 * $Id$
 *
 * Copyright 1996-2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code 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 GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */
package com.sun.javatest;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
import java.util.Date;
import java.util.Vector;

import com.sun.javatest.util.BackupPolicy;
import com.sun.javatest.util.I18NResourceBundle;
import com.sun.javatest.util.StringArray;
import com.sun.javatest.util.Timer;

/**
 * Script is the abstract base class providing the ability to control
 * how a test is to be compiled and executed. In addition to the primary method,
 * <em>run</em>, it has many methods that can be used by subtype classes
 * to assist them in performing a test.
 */
public abstract class Script
{
    /**
     * Initialize any custom args for the script.
     * @param args custom args for the script
     */
    public void initArgs(String[] args) {
        scriptArgs = args;
    }

    /**
     * Initialize the test description to be run by the script.
     * In addition, a mutable test result is set up, in which the results of running
     * the test can be recorded by the script.
     * @param td the test description for the test to be run
     */
    public void initTestDescription(TestDescription td) {
        this.td = td;
        testResult = new TestResult(td);
        trOut = testResult.getTestCommentWriter();
    }

    /**
     * Initialize the list of test cases to be excluded from the test.
     * The script is responsible for determininng how to instruct the test
     * not to run these test cases. A recommended convention is to pass the
     * list of test cases to the test using a -exclude option.
     * @param excludedTestCases a list of test cases within the test that
     * should not be run
     */
    public void initExcludedTestCases(String[] excludedTestCases) {
        this.excludedTestCases = excludedTestCases;
    }

    /**
     * Initialize the environment to be used when running the test.
     * @param env the environment to be used when running the test
     */
    public void initTestEnvironment(TestEnvironment env) {
        this.env = env;
    }

    /**
     * Initialize the work directory to be used to store the results
     * obtained when running the test,
     * and to store any temporary files that may be required by the test.
     * @param workDir the work directory to be used to store the test's results.
     */
    public void initWorkDir(WorkDirectory workDir) {
        this.workDir = workDir;
    }

    /**
     * Initialize the backup policy to be used when creating a test result
     * file in which to store the results of running this test.
     * @param backupPolicy A backup policy object to be used when
     * creating test result files.
     */
    public void initBackupPolicy(BackupPolicy backupPolicy) {
        this.backupPolicy = backupPolicy;
    }

    /**
     * Initialize the class loader for any commands to be loaded.
     * @param loader a class loader to be used to load any commands or other
     * user-specified classes that may be required.
     */
    public void initClassLoader(ClassLoader loader) {
        this.loader = loader;
    }

    /**
     * Initialize a delegate script object. This should only be used in
     * exceptional circumstances, and is mostly provided for historical purposes.
     * @param s The delegate to be initialized
     * @param scriptArgs the arguments to be passed to the delegate object
     */
    protected void initDelegate(Script s, String[] scriptArgs) {
        s.scriptArgs = scriptArgs;
        // copy rest of values across from self
        s.td = td;
        s.env = env;
        s.workDir = workDir;
        s.backupPolicy = backupPolicy;
        s.loader = loader;
        s.testResult = testResult;
        s.trOut = trOut;
        s.jtrIfPassed = jtrIfPassed;
    }

    /**
     * Initialize the test result for the result of the script execution.
     * Normally, a test result is initialized as a side effect of calling
     * initTestDescription. This method should only be called is special
     * circumstances, and is mostly provided for historical purposes.
     * @param tr The test result to set as the result of the script's execution.
     * @throws IllegalStateException if the test result has already been set.
     * @see #initTestDescription
     */
    protected void initTestResult(TestResult tr) {
        if (testResult != null)
            throw new IllegalStateException();

        testResult = tr;
    }

    /**
     * Run the script, to fill out the test results for the test description
     * given to <code>init</code>. Most implementations will use the default
     * implementation of this method, which delegates to a simpler (abstract)
     * method @link(run(String[],TestDescription, TestEnvironment))
     */
    public void run() {
        if (workDir == null)
            throw new NullPointerException(i18n.getString("script.noWorkDir"));
        if (td == null)
            throw new NullPointerException(i18n.getString("script.noTestDesc"));
        if (testResult == null)
            throw new NullPointerException(i18n.getString("script.noTestRslt"));
        if (env == null)
            throw new NullPointerException(i18n.getString("script.noTestEnv"));

        Status execStatus = null;

        // "work" has the the work dir for the suite
        // "testWork" has the work dir for this test
        File testWork = workDir.getFile(td.getRootRelativeDir().getPath());

        // synchronize against interference by other scripts
        synchronized (Script.class) {
            if (!testWork.exists()) {
                testWork.mkdirs();
            }
        }

        String descUrl = td.getFile().toURI().toASCIIString();
        String id = td.getId();
        if (id != null)
            descUrl += "#" + id;
        testResult.putProperty(TestResult.DESCRIPTION, descUrl);
        testResult.putProperty(TestResult.START, (new Date()).toString());
        testResult.putProperty(TestResult.VERSION, ProductInfo.getVersion());
        testResult.putProperty(TestResult.WORK, testWork.getAbsolutePath());
        testResult.putProperty(TestResult.ENVIRONMENT, env.getName());
        if (osInfo == null) {
            String osArch = System.getProperty("os.arch");
            String osName = System.getProperty("os.name");
            String osVersion = System.getProperty("os.version");
            osInfo = osName + " " + osVersion + " (" + osArch + ")";
        }
        testResult.putProperty(TestResult.JAVATEST_OS, osInfo);
        if (excludedTestCases != null)
            testResult.putProperty("excludedTestCases", StringArray.join(excludedTestCases));

        String classDir = td.getParameter("classDir");
        File f = (classDir == null ? workDir.getFile(defaultClassDir) :
                 new File(testWork, classDir));
        env.putUrlAndFile("testClassDir", f);
        env.putUrlAndFile("testWorkDir", testWork);
        env.put("test", td.getFile().getPath());
        env.put("testDir", td.getFile().getParent());
        env.put("testURL", descUrl);
        env.put("testPath", td.getRootRelativeURL());

        int timeout = getTestTimeout();
        PrintStream out = System.out;
        PrintStream err = System.err;

        try {
            testResult.putProperty(TestResult.TEST, td.getRootRelativeURL());
            testResult.putProperty(TestResult.SCRIPT, this.getClass().getName() + " " +
                       StringArray.join(scriptArgs));

            if (timeout > 0) {
                testResult.putProperty("timeoutSeconds", Integer.toString(timeout));
                setAlarm(timeout*1000);
            }

            execStatus = run(scriptArgs, td, env);
        }
        finally {
            if (timeout > 0)
                setAlarm(0);

            try {
                System.setOut(System.out);
                System.setErr(System.err);
            }
            catch (SecurityException ignore) {
            }

            if (Thread.interrupted()) // will clear interrupted status of thread, as desired
                execStatus = Status.error(i18n.getString("script.interrupted"));

            testResult.putProperty(TestResult.END, (new Date()).toString());

            if (execStatus == null) {
                execStatus = Status.error(i18n.getString("script.noStatus"));
            }
            else {
                switch (execStatus.getType()) {
                case Status.PASSED:
                case Status.FAILED:
                case Status.ERROR:
                    break;
                default:
                    execStatus = Status.error(i18n.getString("script.badTestStatus", execStatus));
                }
            }

        }

        testResult.setEnvironment(env);
        testResult.setStatus(execStatus);

        try {
            if (execStatus.getType() != Status.PASSED || jtrIfPassed)
                testResult.writeResults(workDir, backupPolicy);
        }
        catch (IOException e) {
            // ignore it; the test will have an error status already
            //throw new JavaTestError("Unable to write result file! " + e);
        }
    }

    /**
     * The primary method to be provided by Scripts. It is responsible for compiling
     * and executing the test appropiately.  Normally, a script should call `init' and
     * then decode any script-specific options it is given in `args'. It should then
     * examine the test description it is given so that it can compile and execute
     * the test as appropriate. Various convenience routines are provided to
     * simplify the task of running the compiler, an interpreter or any other commands,
     * which can be specified in a flexible manner by properties in the TestEnvironment.
     *
     * @param args      Any script-specific options specified in the script property
     * @param td        The test description for the test to be performed
     * @param env       The test environment giving the details of how to run the test
     * @return          The result of running the script
     * @see #compileIndividually
     * @see #compileTogether
     * @see #execute
     * @see #invokeCommand
     */
    public abstract Status run(String[] args, TestDescription td, TestEnvironment env);

    /**
     * Get the test description for the test which this script will run.
     * @return the test description for the test which this script will run.
     */
    public TestDescription getTestDescription() {
        return td;
    }

    /**
     * Get the test result object to be used for the results of the test run.
     * @return the test result object to be used for the results of the test run.
     */
    public TestResult getTestResult() {
        return testResult;
    }

    /**
     * Get the flag that indicates whether a result (.jtr) file should be written
     * even if the test has passed. By default, this is true.
     * @return the flag that indicates whether a result (.jtr) file should be written
     * even if the test has passed.
     * @see #setJTRIfPassed
     */
    public boolean getJTRIfPassed() {
        return jtrIfPassed;
    }

    /**
     * Set the flag that indicates whether a result (.jtr) file should be written
     * even if the test has passed. By default, this is true.
     * @param b the flag that indicates whether a result (.jtr) file should be written
     * even if the test has passed.
     * @see #getJTRIfPassed
     */
    public void setJTRIfPassed(boolean b) {
        jtrIfPassed = b;
    }

    /**
     * Set an alarm that will interrupt the calling thread after
     * a specified delay (in milliseconds), and repeatedly thereafter
     * until cancelled.
     * Typical usage:
     * <pre>
     * try {
     *     setAlarm(delay);
     *     ...
     * }
     * finally {
     *     setAlarm(0);
     * }
     * </pre>
     * @param timeout the interval (in milliseconds) after which the calling
     * thread will be interrupted, if not cancelled in the meantime.
     */
    protected void setAlarm(int timeout) {
        setAlarm(timeout, Thread.currentThread());
    }


    /**
     * Set an alarm that will interrupt a given thread after
     * a specified delay (in milliseconds), and repeatedly thereafter
     * until cancelled.
     * Typical usage:
     * <pre>
     * try {
     *     setAlarm(delay);
     *     ...
     * }
     * finally {
     *     setAlarm(0);
     * }
     * </pre>
     * @param timeout the interval (in milliseconds) after which the calling
     *      thread will be interrupted, if not cancelled in the meantime.
     * @param threadToInterrupt which thread to interrupt
     */
    protected void setAlarm(int timeout, Thread threadToInterrupt) {
        if (alarm != null) {
            alarm.cancel();
            alarm = null;
        }

        if (timeout > 0)
            alarm = new Alarm(timeout, threadToInterrupt);
    }

    /**
     * Get the timeout to be used for a test.
     * The default implementation is 10 minutes scaled by
     * a value found in the environment ("javatestTimeoutFactor").
     * Override this method to provide different behaviors.
     * A value of zero means no timeout.
     * @return the number of seconds in which the test is expected to
     * complete its execution.
     */
    protected int getTestTimeout() {
        float factor = 1;
        try {
            String[] jtf = env.lookup("javatestTimeoutFactor");
            if (jtf != null) {
                if (jtf.length == 1)
                    factor = Float.parseFloat(jtf[0]);
                else if (jtf.length == 2)
                    factor = Float.parseFloat(jtf[1]);
            }
        }
        catch (TestEnvironment.Fault e) {
        }
        return (int) (10 * 60 * factor);
    }

    /**
     * Compile the given source files individually. One at a time, each source file
     * is passed to <em>compileTogether</em>, until they have all been
     * successfully compiled, or until one fails to compile.
     * @param srcs      The names of the file to be compiled.
     * @return          The status of the compilation: passed or failed.
     * @see #compileTogether
     */
    protected Status compileIndividually(String[] srcs) {
        return compileIndividually(DEFAULT_COMPILE_COMMAND, srcs);
    }

    /**
     * Compile the given source files individually. One at a time, each source file
     * is passed to <em>compileTogether</em>, until they have all been
     * successfully compiled, or until one fails to compile.
     * @param command the base name of the command entry in the environment to be used
     * to compile any necessary sources. The complete entry name will be
     * <code>command.</code><i>command</i><code>.</code><i>extn</i>
     * @param srcs      The names of the file to be compiled.
     * @return          The status of the compilation: passed or failed.
     * @see #compileTogether
     */
    protected Status compileIndividually(String command, String[] srcs) {
        if (srcs.length == 0)
            return error_noSource;

        for (int i = 0; i < srcs.length; i++) {
            Status s = compileOne(command, srcs[i]);
            if (!s.isPassed())
                return s;
        }
        return pass_compSuccExp;
    }

    /**
     * Compile the given source files individually. One at a time, each source file
     * is passed to <em>compileTogether</em>, until they have all been
     * successfully compiled, or until one fails to compile.
     * @param srcs      The names of the file to be compiled.
     * @return          The status of the compilation: passed or failed.
     * @see #compileTogether
     */
    protected Status compileIndividually(File[] srcs) {
        return compileIndividually(DEFAULT_COMPILE_COMMAND, filesToStrings(srcs));
    }

    /**
     * Compile the given source files individually. One at a time, each source file
     * is passed to <em>compileTogether</em>, until they have all been
     * successfully compiled, or until one fails to compile.
     * @param command the base name of the command entry in the environment to be used
     * to compile any necessary sources. The complete entry name will be
     * <code>command.</code><i>command</i><code>.</code><i>extn</i>
     * @param srcs      The names of the file to be compiled.
     * @return          The status of the compilation: passed or failed.
     * @see #compileTogether
     */
    protected Status compileIndividually(String command, File[] srcs) {
        return compileIndividually(command, filesToStrings(srcs));
    }

    /**
     * Compile the given source file.
     * @param src       The name of the file to be compiled.
     * @return          The status of the compilation: passed or failed.
     * @see #compileTogether
     */
    protected Status compileOne(String src) {
        return compileOne(DEFAULT_COMPILE_COMMAND, src);
    }

    /**
     * Compile the given source file. The file is treated as a singleton group
     * and passed to <em>compileTogether</em>.
     * @param command the base name of the command entry in the environment to be used
     * to compile any necessary sources. The complete entry name will be
     * <code>command.</code><i>command</i><code>.</code><i>extn</i>
     * @param src       The name of the file to be compiled.
     * @return          The status of the compilation: passed or failed.
     * @see #compileTogether
     */
    protected Status compileOne(String command, String src) {
        return compileTogether(command, new String[] {src});
    }

    /**
     * Compiles the given source file.
     *
     * @param src       The name of the file to be compiled.
     * @return          The status of the compilation: passed or failed.
     * @see #compileTogether
     */
    protected Status compileOne(File src) {
        return compileOne(DEFAULT_COMPILE_COMMAND, src.getPath());
    }

    /**
     * Compiles the given source file.
     *
     * @param command the base name of the command entry in the environment to be used
     * to compile any necessary sources. The complete entry name will be
     * <code>command.</code><i>command</i><code>.</code><i>extn</i>
     * @param src       The name of the file to be compiled.
     * @return          The status of the compilation: passed or failed.
     * @see #compileTogether
     */
    protected Status compileOne(String command, File src) {
        return compileOne(command, src.getPath());
    }

    /**
     * Compile the given source files together.  The compiler and arguments to be used
     * are identified by the `<code>env.<em>env</em>.compile.<em>extn</em>.*</code>'
     * properties in the script's environment, where <em>env</em>
     * is the name of the environment specified to the GUI, and <em>extn</em> is
     * the extension of the first source file.  The names of the files to be compiled
     * are added to the end of the arguments retrieved from the environment.
     * @param srcs      The names of the file to be compiled.
     * @return          The status of the compilation: passed or failed.
     * @see #invokeCommand
     */
    protected Status compileTogether(String[] srcs) {
        return compileTogether(DEFAULT_COMPILE_COMMAND, srcs);
    }

    /**
     * Compile the given source files together.  The compiler and arguments to be used
     * are identified by the `<code>env.<em>env</em>.command.<em>command</em>.<em>extn</em>.*</code>'
     * properties in the script's environment, where <em>env</em>
     * is the name of the environment specified to the GUI, and <em>extn</em> is
     * the extension of the first source file.  The names of the files to be compiled
     * are added to the end of the arguments retrieved from the environment.
     * @param command the base name of the command entry in the environment to be used
     * to compile any necessary sources. The complete entry name will be
     * <code>command.</code><i>command</i><code>.</code><i>extn</i>
     * @param srcs      The names of the file to be compiled.
     * @return          The status of the compilation: passed or failed.
     * @see #invokeCommand
     */
    protected Status compileTogether(String command, String[] srcs) {
        if (srcs.length == 0)
            return error_noSource;

        try {
            String[] classDir = env.lookup("testClassDir");
            if (classDir == null || classDir.length != 1)
                return error_badTestClassDir;
            File f = new File(classDir[0]);
            if (!f.exists())
                f.mkdirs();
        }
        catch (TestEnvironment.Fault e) {
            return error_badTestClassDir;
        }

        String primarySrcFile = srcs[0];
        int dot = primarySrcFile.lastIndexOf('.');
        if (dot == -1)
            return error_noExtnInSource;

        String extn = primarySrcFile.substring(dot);

        env.put("testSource", srcs);

        try {
            boolean ok = sourceTable.acquire(srcs, 10*60*1000);
            if (!ok)
                return Status.error(i18n.getString("script.srcLockTimeout"));
            return invokeCommand(command + extn);
        }
        catch (InterruptedException e) {
            return Status.error(i18n.getString("script.srcLockInterrupted"));
        }
        finally {
            sourceTable.release(srcs);
        }
    }

    private static ResourceTable sourceTable = new ResourceTable();

    /**
     * Compile the given source files together.  The compiler and arguments to be used
     * are identified by the `<code>env.<em>env</em>.command.compile.<em>extn</em>.*</code>'
     * properties in the script's environment, where <em>env</em>
     * is the name of the environment specified to the GUI, and <em>extn</em> is
     * the extension of the first source file.  The names of the files to be compiled
     * are added to the end of the arguments retrieved from the environment.
     * @param srcs      The names of the file to be compiled.
     * @return          The status of the compilation: passed or failed.
     * @see #invokeCommand
     */
    protected Status compileTogether(File[] srcs) {
        return compileTogether(DEFAULT_COMPILE_COMMAND, filesToStrings(srcs));
    }

    /**
     * Compile the given source files together.  The compiler and arguments to be used
     * are identified by the `<code>env.<em>env</em>.command.<em>command</em>.<em>extn</em>.*</code>'
     * properties in the script's environment, where <em>env</em>
     * is the name of the environment specified to the GUI, and <em>extn</em> is
     * the extension of the first source file.  The names of the files to be compiled
     * are added to the end of the arguments retrieved from the environment.
     * @param command the base name of the command entry in the environment to be used
     * to compile any necessary sources. The complete entry name will be
     * <code>command.</code><i>command</i><code>.</code><i>extn</i>
     * @param srcs      The names of the file to be compiled.
     * @return          The status of the compilation: passed or failed.
     * @see #invokeCommand
     */
    protected Status compileTogether(String command, File[] srcs) {
        return compileTogether(command, filesToStrings(srcs));
    }

    /**
     * Compile those source files for which the corresponding class file appears to
     * be out of date. Each source file is scanned to find a package statement to help
     * determine the main class defined in the source file -- the corresponding class
     * file in the given class directory is then checked, and if the source file is newer,
     * it is put on a list to be recompiled. After checking all the source files, if any
     * need to be recompiled, they will be compiled together, using the default compile
     * command ("command.compile.extn") entry in the the environment.
     * @param srcs The names of the source files to be compiled if necessary
     * @param classDir The class directory in which the corresponding class files
     * (if any) will be found.
     * @return          The status of the compilation: passed or failed.
     * @see #compileTogether
     */
    protected Status compileIfNecessary(String[] srcs, String classDir) {
        return compileIfNecessary(DEFAULT_COMPILE_COMMAND, srcs, classDir);
    }

    /**
     * Compile those source files for which the corresponding class file appears to
     * be out of date. Each source file is scanned to find a package statement to help
     * determine the main class defined in the source file -- the corresponding class
     * file in the given class directory is then checked, and if the source file is newer,
     * it is put on a list to be recompiled. After checking all the source files, if any
     * need to be recompiled, they will be compiled together, using the specified compile
     * command entry in the the environment.
     * @param command the base name of the command entry in the environment to be used
     * to compile any necessary sources. The complete entry name will be
     * <code>command.</code><i>command</i><code>.</code><i>extn</i>
     * @param srcs The names of the source files to be compiled if necessary
     * @param classDir The class directory in which the corresponding class files
     * (if any) will be found.
     * @return          The status of the compilation: passed or failed.
     * @see #compileTogether
     */
    protected Status compileIfNecessary(String command, String[] srcs, String classDir) {
        if (srcs.length == 0)
            return error_noSource;

        if (classDir == null)
            classDir = "$testClassDir";

        if (classDir.startsWith("$")) {
            try {
                String[] cd = env.resolve(classDir);
                if (cd == null || cd.length != 1)
                    return error_badTestClassDir;
                classDir = cd[0];
            }
            catch (TestEnvironment.Fault e) {
                return error_badTestClassDir;
            }
        }

        File cdf = new File(classDir);
        if (!cdf.exists())
            cdf.mkdirs();

        Vector v = new Vector(0, srcs.length);

        for (int i = 0; i < srcs.length; i++) {
            String src = srcs[i];
            int x = src.lastIndexOf(File.separatorChar);
            int y = src.indexOf('.', x+1);
            String className = src.substring(x+1, (y == -1 ? src.length() : y));
            String pkgPrefix;  // deliberately unset to have compiler check init in all required cases

            // read the source file to see if a package statement exists
            // if it does, set pkgPrefix to package directory
            // if none found, set setPrefix to empty string
            // if error, report the error, ignore pkgPrefix, and set file to
            // be unconditionally compiled.
            BufferedReader r = null;
            try {
                r = new BufferedReader(new FileReader(src));
                StreamTokenizer tr = new StreamTokenizer(r);
                tr.ordinaryChar('/');
                tr.slashStarComments(true);
                tr.slashSlashComments(true);
                tr.wordChars('.', '.'); // package separator
                int c = tr.nextToken();
                if (c == StreamTokenizer.TT_WORD && tr.sval.equals("package")) {
                    // found what looks like a package statement
                    c = tr.nextToken();
                    if (c == StreamTokenizer.TT_WORD)
                        // yes, it was a valid package statement
                        pkgPrefix = tr.sval.replace('.', File.separatorChar) + File.separatorChar;
                    else {
                        // well, sort of; malformed package statement
                        trOut.println(i18n.getString("script.badPackage"));
                        v.addElement(src);
                        continue;
                    }
                }
                else
                    // no package statement
                    pkgPrefix = "";
            }
            catch (IOException e) {
                trOut.println(i18n.getString("script.badDateStamp", new Object[] { src, e }));
                v.addElement(src);
                continue;
            }
            finally {
                if (r != null) {
                    try {
                        r.close();
                    }
                    catch (IOException ignore) {
                    }
                }
            }


            File srcFile = new File(src);
            File classFile = new File(classDir, pkgPrefix + className + ".class");
            //System.out.println("checking " + classFile);
            //System.out.println("classfile " + classFile.lastModified());
            //System.out.println("srcfile " + srcFile.lastModified());
            if (classFile.exists() && classFile.lastModified() > srcFile.lastModified())
                trOut.println(i18n.getString("script.upToDate", src));
            else
                v.addElement(src);
        }

        if (v.size() > 0) {
            String[] necessarySrcs = new String[v.size()];
            v.copyInto(necessarySrcs);

            return compileTogether(command, necessarySrcs);
        }
        else
            return Status.passed(i18n.getString("script.allUpToDate"));
    }

    /**
     * Compile those source files for which the corresponding class file appears to
     * be out of date. Each source file is scanned to find a package statement to help
     * determine the main class defined in the source file -- the corresponding class
     * file in the given class directory is then checked, and if the source file is newer,
     * it is put on a list to be recompiled. After checking all the source files, if any
     * need to be recompiled, they will be compiled together, using the default compile
     * command ("command.compile.extn") entry in the the environment.
     * @param srcs The names of the source files to be compiled if necessary
     * @param classDir The class directory in which the corresponding class files
     * (if any) will be found.
     * @return          The status of the compilation: passed or failed.
     * @see #compileTogether
     */
    protected Status compileIfNecessary(File[] srcs, String classDir) {
        return compileIfNecessary(DEFAULT_COMPILE_COMMAND, filesToStrings(srcs), classDir);
    }

    /**
     * Compile those source files for which the corresponding class file appears to
     * be out of date. Each source file is scanned to find a package statement to help
     * determine the main class defined in the source file -- the corresponding class
     * file in the given class directory is then checked, and if the source file is newer,
     * it is put on a list to be recompiled. After checking all the source files, if any
     * need to be recompiled, they will be compiled together, using the specified compile
     * command entry in the the environment.
     * @param command the base name of the command entry in the environment to be used
     * to compile any necessary sources. The complete entry name will be
     * <code>command.</code><i>command</i><code>.</code><i>extn</i>
     * @param srcs The names of the source files to be compiled if necessary
     * @param classDir The class directory in which the corresponding class files
     * (if any) will be found.
     * @return          The status of the compilation: passed or failed.
     * @see #compileTogether
     */
    protected Status compileIfNecessary(String command, File[] srcs, String classDir) {
        return compileIfNecessary(command, filesToStrings(srcs), classDir);
    }

    /**
     * Execute the given class with the given arguments, which need to be passed
     * to the environment for $ substitution and for splitting into separate strings.
     * @param executeClass      The name of the class to be executed
     * @param executeArgs       The arguments to be evaluated before passing to
     *                          the class to be executed
     * @return                  The status of the execution
     * @see #execute(java.lang.String, java.lang.String, java.lang.String)
     */
    protected Status execute(String executeClass, String executeArgs) {
        return execute(DEFAULT_EXECUTE_COMMAND, executeClass, executeArgs);
    }

    /**
     * Execute the given class with the given arguments, which need to be passed
     * to the environment for $ substitution and for splitting into separate strings.
     * @param command   The name of the command containing the template to be executed
     * @param executeClass      The name of the class to be executed
     * @param executeArgs       The arguments to be evaluated before passing to
     *                          the class to be executed
     * @return                  The status of the execution
     */
    protected Status execute(String command, String executeClass,
                             String executeArgs) {
        try {
            String[] args = (executeArgs == null ? nullArgs : env.resolve(executeArgs));
            if (excludedTestCases != null)
                args = exclude(args, excludedTestCases);
            return execute(command, executeClass, args);
        }
        catch (TestEnvironment.Fault e) {
            trOut.println(i18n.getString("script.testEnvFault",
                                         new Object[] { executeArgs, e.toString() }));
            return error_badExecuteArgs;
        }
    }

    /**
     * Execute the given class with the given arguments.  The interpreter to be used
     * and its arguments are identified by the `<code>env.<em>env</em>.execute.*</code>'
     * properties in the script's environment, where <em>env</em>
     * is the name of the environment specified to the GUI. The class to be executed and
     * its arguments are added to the end of the arguments retrieved from the environment.
     * @param executeClass      The name of the class to be executed.
     * @param executeArgs       Any arguments to be passed to the class to be executed.
     * @return                  The status of the execution
     * @see #execute(java.lang.String, java.lang.String, java.lang.String[])
     */
    protected Status execute(String executeClass, String[] executeArgs) {
        return execute(DEFAULT_EXECUTE_COMMAND, executeClass, executeArgs);
    }

    /**
     * Execute the given class with the given arguments.  The interpreter to be used
     * and its arguments are identified by the `<code>env.<em>env</em>.<em>command</em>.*</code>'
     * properties in the script's environment, where <em>env</em>
     * is the name of the environment specified to the GUI. The class to be executed and
     * its arguments are added to the end of the arguments retrieved from the environment.
     * @param command   The name of the command containing the template to be executed
     * @param executeClass      The name of the class to be executed.
     * @param executeArgs       Any arguments to be passed to the class to be executed.
     * @return                  The status of the execution
     * @see #invokeCommand
     */
    protected Status execute(String command, String executeClass, String[] executeArgs) {
        if (executeClass == null || executeClass.length() == 0)
            return error_noExecuteClass;
        env.put("testExecuteClass", executeClass);
        env.put("testExecuteArgs", executeArgs);
        return invokeCommand(command);
    }

    /**
     * RMI Compile the given class files.  The compiler and arguments to be used
     * is identified by the `<code>env.<em>env</em>.command.rmic</code>'
     * property in the script's environment, where <em>env</em>
     * is the name of the environment specified to the GUI.
     * The name of the classes to be compiled by rmic is obtained from the
     * test description.
     * @param classes   The names of the classes to be compiled by rmic.
     * @return          The status of the compilation: passed or failed.
     * @see #invokeCommand
     */
    protected Status rmiCompile(String[] classes) {
        return rmiCompile(DEFAULT_RMIC_COMMAND, classes);
    }

    /**
     * RMI Compile the given class files.  The compiler and arguments to be used
     * is identified by the `<code>env.<em>env</em>.command.<em>command</em></code>'
     * property in the script's environment, where <em>env</em>
     * is the name of the environment specified to the GUI.
     * The name of the classes to be compiled by rmic is obtained from the
     * test description.
     * @param command   The name of the command containing the template to be compiled
     * @param classes   The names of the classes to be compiled by rmic.
     * @return          The status of the compilation: passed or failed.
     * @see #invokeCommand
     */
    protected Status rmiCompile(String command, String[] classes) {
        try {
            String[] classDir = env.lookup("testClassDir");
            if (classDir == null || classDir.length != 1)
                return error_badTestClassDir;
            File f = new File(classDir[0]);
            if (!f.exists())
                f.mkdirs();
        }
        catch (TestEnvironment.Fault e) {
            return error_badTestClassDir;
        }

        if (classes == null || classes.length == 0)
            return error_noRMIClasses;

        env.put("testRmicClasses", classes);
        // backwards compatability
        env.put("testRmicClass", classes);
        return invokeCommand(command);
    }


    /**
     * Invoke a command in the environment identified by a given key.
     * The command is identified by looking up `<code>command.<em>key</em></code>'
     * property in the environment. The first word of this property identifies
     * the name of a class that should be an implementation of <code>Command</code>,
     * and the subsequent words are the arguments to be passed to a fresh instance
     * of that class, via its <code>run</code> method.
     * Standard library implementations of <code>Command</code> are available,
     * such as:
     * <DL>
     * <DT>com.sun.javatest.lib.ProcessCommand
     * <DD>Execute a command in a separate process
     * <DT>com.sun.javatest.lib.ExecStdTestSameJVMCmd
     * <DD>Execute a standard test in the same JVM as JT Harness
     * <DT>com.sun.javatest.agent.PassiveAgentCommand
     * <DD>Execute a command on a remote machine
     * </DL>
     * For full details, the documentation for the various appropriate classes.
     *
     * <p> The use of `<code>command.<em>key</em></code>' supercedes an earlier
     * mechanism involving multiple properties. For backwards compatability,
     * if the `<code>command.<em>key</em></code>' property is not found, the
     * properties for the earlier mechanism are checked as well.
     *
     * @param key       The tag for the command to be executed
     * @return          A status giving the outcome of the command
     *
     * @see Command
     *
     */
    protected Status invokeCommand(String key) {
        TestResult.Section section;
        Status s = null;

        try {
            String[] command = env.lookup("command." + key);

            if (command.length == 0)
                return Status.error(i18n.getString("script.noCommand",
                                                   new Object[] { env.getName(), key }));

            String className = command[0];
            String[] args = new String[command.length - 1];
            System.arraycopy(command, 1, args, 0, args.length);

            section = testResult.createSection(key);

            section.getMessageWriter().println(i18n.getString("script.command",
                                                              new Object[] {className, StringArray.join(args) }));

            PrintWriter out1 = null;
            PrintWriter out2 = null;
            try {
                out1 = section.createOutput(cmdOut1Name);
                out2 = section.createOutput(cmdOut2Name);

                s = invokeClass(className, args, out1, out2);

                out1.close();
                out2.close();
            }
            finally {
                if (out2 != null)  out2.close();
                if (out1 != null)  out1.close();
            }

            section.setStatus(s);
            return s;
        }
        catch (TestEnvironment.Fault e) {
            return Status.error(i18n.getString("script.badCommand",
                                               new Object[] { env.getName(), key }));
        }
    }

    /**
     * Set the default names of the two default output streams used when executing a
     * command.  In many cases these may correspond to the UNIX-style standard-out
     * and standard-error streams.  This API does not define what they are used for
     * though, and architects are encouraged to give descriptive names if possible.
     *
     * @param out1Name Name of the first stream.
     * @param out2Name Name of the second stream.
     */
    protected void setDefaultCommandStreamNames(String out1Name, String out2Name) {
        cmdOut1Name = out1Name;
        cmdOut2Name = out2Name;
    }

    /**
     * Create and run a Command object.
     * @param className The name of the class to load and instantiate.
     * @param args      The args to pass to the `run' method of the loaded object.
     * @return          The result identifies any problems that may occur in trying
     *                  to create and run the specified object, or if it succeeds,
     *                  it returns the result from calling the object's `run' method.
     * @see Command
     */
    private Status invokeClass(String className, String[] args,
                               PrintWriter out1, PrintWriter out2) {
        // this is the central place where we get to run what the user
        // says in the environment file:
        Command testCommand;
        try {
            Class c = (loader == null ? Class.forName(className) : loader.loadClass(className));
            testCommand = (Command)(c.newInstance());
        }
        catch (ClassCastException e) {
            return Status.error(i18n.getString("script.cantRunClass",
                                               new Object[] { className, Command.class.getName() }));
        }
        catch (ClassNotFoundException ex) {
            return Status.error(i18n.getString("script.cantFindClass",
                                               new Object[] { className, env.getName() }));
        }
        catch (IllegalAccessException ex) {
            return Status.error(i18n.getString("script.cantAccessClass",
                                               new Object[] { className, env.getName() }));
        }
        catch (IllegalArgumentException ex) {
            return Status.error(i18n.getString("script.badClassName",
                                               new Object[] { className, env.getName() }));
        }
        catch (InstantiationException ex) {
            return Status.error(i18n.getString("script.cantCreateClass",
                                               new Object[] { className, env.getName() }));
        }
        catch (ThreadDeath e) {
            throw (ThreadDeath)(e.fillInStackTrace());
        }
        catch (Exception e) {
            e.printStackTrace(out1);
            return Status.error(i18n.getString("script.unexpLoadExc", new Object[] { className, e }));
        }
        catch (Error e) {
            e.printStackTrace(out1);
            return Status.error(i18n.getString("script.unexpLoadErr", new Object[] { className, e }));
        }
        catch (Throwable e) {
            e.printStackTrace(out1);
            return Status.error(i18n.getString("script.unexpLoadThr", new Object[] { className, e }));
        }

        try {
            testCommand.setClassLoader(loader);
            return testCommand.run(args, out1, out2);
        }
        catch (ThreadDeath e) {
            throw (ThreadDeath)(e.fillInStackTrace());
        }
        catch (Exception e) {
            e.printStackTrace(out1);
            // error reduced to failed in following line for benefit of negative tests
            return Status.failed(i18n.getString("script.unexpExecExc", new Object[] { className, e }));
        }
        catch (Error e) {
            e.printStackTrace(out1);
            // error reduced to failed in following line for benefit of negative tests
            return Status.failed(i18n.getString("script.unexpExecErr", new Object[] { className, e }));
        }
        catch (Throwable e) {
            e.printStackTrace(out1);
            // error *NOT* reduced to failed in following line for benefit of
            // negative tests: test should never throw something which is not
            // an Exception or Error
            return Status.error(i18n.getString("script.unexpExecThr", new Object[] { className, e }));
        }

    }

    /**
     * Modify the args for a test to be executed, according to a set
     * of test cases to be excluded. If there are no test cases to be excluded,
     * the result will be the original args unchanged; otherwise, the
     * result will be the original args prefixed by "-exclude" and a
     * comma-separated list of exclude test cases.
     * @param args The basic list of args for the test
     * @param testCases the set of test cases to be excluded, or null if none
     * @return The original list of args, possibly prefixed by "-exclude"
     * and a comma-separated list of test cases that should not be executed by
     * the test
     */
    protected String[] exclude(String[] args, String[] testCases) {
        if (testCases == null)
            return args;
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < testCases.length; i++) {
            if (i > 0)
                sb.append(",");
            sb.append(testCases[i]);
        }
        String[] newArgs = new String[args.length + 2];
        newArgs[0] = "-exclude";
        newArgs[1] = sb.toString();
        System.arraycopy(args, 0, newArgs, 2, args.length);
        testResult.putProperty("exclude", newArgs[1]);
        return newArgs;
    }

    /**
     * Utility routine to convert an array of filenames to a corresponding
     * array of strings.
     * @param files     The filenames to be converted
     * @return          The corresponding strings
     */
    protected static String[] filesToStrings(File[] files) {
        String[] strings = new String[files.length];
        for (int i = 0; i < files.length; i++)
            strings[i] = files[i].getPath();
        return strings;
    }

    /**
     * The test description for the test being performed.
     */
    protected TestDescription td;               // required

    /**
     * The set of test cases to be excluded for this test.
     */
    protected String[] excludedTestCases;       // optional, may be null

    /**
     * The test environment for the test being performed.
     */
    protected TestEnvironment env;              // required

    /**
     * The initialization args for the script.
     */
    protected String[] scriptArgs;              // optional

    /**
     * The work directory for the test run.
     */
    protected WorkDirectory workDir;                    // required

    /**
     * The default name for the TestResult section used to save the data written to the out1 stream
     * for a command.
     * @see Command#run
     */
    protected String cmdOut1Name = "out1";

    /**
     * The default name for the TestResult section used to save the data written to the out2 stream
     * for a command.
     * @see Command#run
     */
    protected String cmdOut2Name = "out2";

    /**
     * A backup policy object that specifies how files should be backed up,
     * if a file is found to exist when a new one of the same name is to be
     * written.
     */
    protected BackupPolicy backupPolicy = BackupPolicy.noBackups(); // optional

    /**
     * The class loader to be used to load additional user-specified classes
     * as required in the execution of the script.
     */
    protected ClassLoader loader;               // optional, may be null

    /**
     * The reporting channel for the test being performed.
     */
    protected PrintWriter trOut;

    // have to define this before the definitions that follow
    private static final I18NResourceBundle i18n = I18NResourceBundle.getBundleForClass(Script.class);

    // convenience definitions

    /**
     * A status that may be used to indicate problems in the executeArgs field
     * of a test description.
     */
    protected static final Status
        error_badExecuteArgs = Status.error(i18n.getString("script.badExecuteArgs"));

    /**
     * A status that may be used to indicate a problem with a test's class directory.
     */
    protected static final Status
        error_badTestClassDir = Status.error(i18n.getString("script.badTestClassDir"));

    /**
     * A status that may be used to indicate that a compilation failed unexpectedly.
     */
    protected static final Status
        error_compFailUnexp = Status.error(i18n.getString("script.compFailUnexp"));

    /**
     * A status that may be used to indicate that no action was specified.
     */
    protected static final Status
        error_noActionSpecified = Status.error(i18n.getString("script.noAction"));

    /**
     * A status that may be used to indicate that no execute class was specified in a test description.
     */
    protected static final Status
        error_noExecuteClass = Status.error(i18n.getString("script.noExecuteClass"));

    /**
     * A status that may be used to indicate that no extension was found in a source file.
     */
    protected static final Status
        error_noExtnInSource = Status.error(i18n.getString("script.noExtnInSrc"));

    /**
     * A status that may be used to indicate that no rmi classes were specified in a test description.
     */
    protected static final Status
        error_noRMIClasses = Status.error(i18n.getString("script.noRMIClasses"));

    /**
     * A status that may be used to indicate that no sources were specified in a test description.
     */
    protected static final Status
        error_noSource = Status.error(i18n.getString("script.noSource"));

    /**
     * A status that may be used to indicate the a compilation failed unexpectedly.
     */
    protected static final Status
        fail_compFailUnexp = Status.failed(i18n.getString("script.compFailUnexp"));

    /**
     * A status that may be used to indicate that a compilation did not fail as was expected.
     */
    protected static final Status
        fail_compSuccUnexp = Status.failed(i18n.getString("script.compSuccUnexp"));

    /**
     * A status that may be used to indicate that a test execution step  did not fail as wqas expected.
     */
    protected static final Status
        fail_execSuccUnexp = Status.failed(i18n.getString("script.execSuccUnexp"));

    /**
     * A status that may be used to indicate that a compilation failed as expected.
     */
    protected static final Status
        pass_compFailExp = Status.passed(i18n.getString("script.compFailExp"));

    /**
     * A status that may be used to indicate that a compilation succeeded as expected.
     */
    protected static final Status
        pass_compSuccExp = Status.passed(i18n.getString("script.compSuccExp"));

    /**
     * A status that may be used to indicate that an execution step failed, as was expected.
     */
    protected static final Status
        pass_execFailExp = Status.passed(i18n.getString("script.execFailExp"));

    // backwards compatibility
    /**
     * A status that may be used to indicate that no source files were found in the test description.
     */
    protected static final Status noSource = error_noSource;

    /**
     * A status that may be used to indicate that no extension was found in a source file.
     */
    protected static final Status noExtnInSource = error_noExtnInSource;

    private static final String[] nullArgs = { };
    private static final String DEFAULT_COMPILE_COMMAND = "compile";
    private static final String DEFAULT_EXECUTE_COMMAND = "execute";
    private static final String DEFAULT_RMIC_COMMAND = "rmic";
    private static final String defaultClassDir = "classes";
    private static String osInfo;

    /**
     * A timer that may be used to set up timeouts.
     */
    protected static Timer alarmTimer = new Timer();

    private TestResult testResult;
    private Alarm alarm;
    private boolean jtrIfPassed =
        System.getProperty("javatest.script.jtrIfPassed", "true").equals("true");

    private class Alarm implements Timer.Timeable {
        Alarm(int delay) {
            this(delay, Thread.currentThread());
        }

        Alarm(int delay, Thread threadToInterrupt) {
            if (threadToInterrupt == null)
                throw new NullPointerException();

            this.delay = delay;
            this.threadToInterrupt = threadToInterrupt;
            entry = alarmTimer.requestDelayedCallback(this, delay);
            if (debugAlarm)
                System.err.println(i18n.getString("script.alarm.started", this));
        }

        synchronized void cancel() {
            if (debugAlarm)
                System.err.println(i18n.getString("script.alarm.cancelled", this));
            alarmTimer.cancel(entry);
        }

        public synchronized void timeout() {
            if (count == 0)
                trOut.println(i18n.getString("script.timeout", new Float(delay/1000.f)));
            else if (count%100 == 0) {
                trOut.println(i18n.getString("script.notResponding", new Integer(count)));
                if (count%1000 == 0)
                    System.err.println(i18n.getString("script.timedOut",
                                                      new Object[] { td.getRootRelativeURL(), new Integer(count) }));
            }
            if (debugAlarm)
                System.err.println(i18n.getString("script.alarm.interrupt", new Object[] { this, threadToInterrupt }));
            threadToInterrupt.interrupt();
            count++;
            entry = alarmTimer.requestDelayedCallback(this, 100); // keep requesting interrupts until cancelled
        }

        private int delay;
        private Thread threadToInterrupt;
        private int count;
        private Timer.Entry entry;
    }

    private static boolean debugAlarm = Boolean.getBoolean("debug.com.sun.javatest.Script.Alarm");
}
