/* *******************************************************************
 * Copyright (c) 2002 Palo Alto Research Center, Incorporated (PARC).
 * All rights reserved. 
 * This program and the accompanying materials are made available 
 * under the terms of the Eclipse Public License v1.0 
 * which accompanies this distribution and is available at 
 * http://www.eclipse.org/legal/epl-v10.html 
 *  
 * Contributors: 
 *     Xerox/PARC     initial implementation 
 * ******************************************************************/

package org.aspectj.testing.harness.bridge;

import org.aspectj.bridge.AbortException;
import org.aspectj.bridge.IMessageHandler;
import org.aspectj.bridge.MessageUtil;
import org.aspectj.testing.Tester;
import org.aspectj.testing.run.IRunIterator;
import org.aspectj.testing.run.IRunStatus;
import org.aspectj.testing.run.WrappedRunIterator;
import org.aspectj.testing.util.TestClassLoader;
import org.aspectj.testing.util.TestUtil;
import org.aspectj.testing.xml.SoftMessage;
import org.aspectj.testing.xml.XMLWriter;
import org.aspectj.util.FileUtil;
import org.aspectj.util.LangUtil;
import org.aspectj.weaver.loadtime.WeavingURLClassLoader;

import java.io.*;
import java.lang.reflect.*;
import java.net.*;
import java.security.Permission;
import java.util.*;

/**
 * Run a class in this VM using reflection.
 * Forked mode supported, but through system properties:
 * - javarun.fork: anything to enable forking
 * - javarun.java: path to java executable (optional)
 * - javarun.java.home: JAVA_HOME for java (optional)
 *   (normally requires javarun.java)
 * - javarun.classpath: a prefix to the run classpath (optional)
 */
public class JavaRun implements IAjcRun {

    private static void appendClasspath(StringBuffer cp, Object[] entries) {
        if (!LangUtil.isEmpty(entries)) {
            for (int i = 0; i < entries.length; i++) {
                Object entry = entries[i];
                if (entry instanceof String) {
                    cp.append((String) entry);
                    cp.append(File.pathSeparator);
                } else if (entry instanceof File) {
                    String s = FileUtil.getBestPath((File) entry);
                    if (null != s) {
                        cp.append(s);
                        cp.append(File.pathSeparator);
                    }
                }
            }
        }
    }
    
    Spec spec;
	private Sandbox sandbox;   

    /** programmatic initialization per spec */
    public JavaRun(Spec spec) {
        this.spec = spec;
    }
    // XXX init(Spec)
    
    /**
     * This checks the spec for a class name
     * and checks the sandbox for a readable test source directory,
     * a writable run dir, and (non-null, possibly-empty) lists
     * of readable classpath dirs and jars,
     * and, if fork is enabled, that java can be read.
	 * @return true if all checks pass
     * @see org.aspectj.testing.harness.bridge.AjcTest.IAjcRun#setup(File, File)
	 */
	public boolean setupAjcRun(Sandbox sandbox, Validator validator) {
		this.sandbox = sandbox;
        sandbox.javaRunInit(this);
		return (validator.nullcheck(spec.className, "class name")
            && validator.nullcheck(sandbox, "sandbox")
            && validator.canReadDir(sandbox.getTestBaseSrcDir(this), "testBaseSrc dir")
            && validator.canWriteDir(sandbox.runDir, "run dir")
            && validator.canReadFiles(sandbox.getClasspathJars(true, this), "classpath jars")
            && validator.canReadDirs(sandbox.getClasspathDirectories(true, this, true), "classpath dirs")
            && (!spec.forkSpec.fork
                    || validator.canRead(spec.forkSpec.java, "java"))
            );            
        
	}
    
    /** caller must record any exceptions */
    public boolean run(IRunStatus status)
        throws IllegalAccessException,
                InvocationTargetException,
                ClassNotFoundException,
                NoSuchMethodException {
        boolean completedNormally = false;
        boolean passed = false;
        if (!LangUtil.isEmpty(spec.dirChanges)) {
            MessageUtil.info(status, "XXX dirChanges not implemented in JavaRun");
        }
        try {
            final boolean readable = true;
            File[] libs = sandbox.getClasspathJars(readable, this);
            boolean includeClassesDir = true;
            File[] dirs = sandbox.getClasspathDirectories(readable, this, includeClassesDir);
            completedNormally = (spec.forkSpec.fork)
                ? runInOtherVM(status, libs, dirs)
                : runInSameVM(status, libs, dirs);
            passed = completedNormally;
        } finally {
            if (!passed  || !status.runResult()) {
                MessageUtil.info(status, spec.toLongString());
                MessageUtil.info(status, "sandbox: " + sandbox);
            }
        }
        return passed;
    }
    protected boolean runInSameVM(IRunStatus status, File[] libs, File[] dirs) throws SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
        ClassLoader loader = null;
        boolean completedNormally = false;
        boolean passed = false;
        ByteArrayOutputStream outSnoop = null;
        PrintStream oldOut = null;
        ByteArrayOutputStream errSnoop = null;
        PrintStream oldErr = null;
        if (spec.outStreamIsError) {
            outSnoop = new ByteArrayOutputStream();
            oldOut = System.out;
            System.setOut(new PrintStream(outSnoop, true));
        }
        if (spec.errStreamIsError) {
            errSnoop = new ByteArrayOutputStream();
            oldErr = System.err;
            System.setErr(new PrintStream(errSnoop, true));
        }
        Class targetClass = null;
        try {
            final URL[] clAndLibs;
            {
                File[] files = sandbox.findFiles(spec.classpath);
                URL[] clURLs = FileUtil.getFileURLs(files);
                URL[] libURLs = FileUtil.getFileURLs(libs);
                clAndLibs = new URL[clURLs.length + libURLs.length];
                System.arraycopy(clURLs, 0, clAndLibs , 0, clURLs.length);
                System.arraycopy(libURLs, 0, clAndLibs, clURLs.length, libURLs.length);
            }
            if (!spec.isLTW()) {
                loader = new TestClassLoader(clAndLibs, dirs);                
            } else {
                final URL[] aspectURLs;
                {
                    File[] files = sandbox.findFiles(spec.aspectpath);
                    aspectURLs = FileUtil.getFileURLs(files);
                }
                ArrayList classpath = new ArrayList();
                classpath.addAll(Arrays.asList(aspectURLs));
                final URL[] classURLs;
                {
                    classpath.addAll(Arrays.asList(clAndLibs));
                    URL[] urls = FileUtil.getFileURLs(dirs);
                    classpath.addAll(Arrays.asList(urls));
                    classpath.add(FileUtil.getFileURL(Globals.F_aspectjrt_jar));
                    classpath.add(FileUtil.getFileURL(Globals.F_testingclient_jar));
                    classURLs = (URL[]) classpath.toArray(new URL[0]);
                }
                
                ClassLoader parent = JavaRun.class.getClassLoader();
                loader = new WeavingURLClassLoader(classURLs, aspectURLs, parent);
            }
            // make the following load test optional
            // Class testAspect = loader.loadClass("org.aspectj.lang.JoinPoint");
            targetClass = loader.loadClass(spec.className);
            Method main = targetClass.getMethod("main", Globals.MAIN_PARM_TYPES);
            setupTester(sandbox.getTestBaseSrcDir(this), loader, status);
            RunSecurityManager.ME.setJavaRunThread(this);
            main.invoke(null, new Object[] { spec.getOptionsArray() });
            completedNormally = true;
            boolean snoopFailure =
                ((null != errSnoop) && 0 < errSnoop.size())
                || ((null != outSnoop) && 0 < outSnoop.size());
            passed = !snoopFailure && (null == spec.expectedException);
        } catch (AbortException e) {
            if (expectedException(e)) {
                passed = true;
            } else {
                throw e;
            }
        } catch (InvocationTargetException e) {
            // this and following clauses catch ExitCalledException
            Throwable thrown = LangUtil.unwrapException(e);
            if (null == thrown) {
                throw e;
            }
            if (thrown instanceof RunSecurityManager.ExitCalledException) {
                int i = ((RunSecurityManager.ExitCalledException) thrown).exitCode;
                status.finish(new Integer(i));
            } else if (thrown instanceof RunSecurityManager.AwtUsedException) {
                MessageUtil.fail(status, "test code should not use the AWT event queue");
                throw (RunSecurityManager.AwtUsedException) thrown;
                // same as: status.thrown(thrown);
            } else if (expectedException(thrown)) {
                passed = true;
            } else if (thrown instanceof RuntimeException) {
                throw (RuntimeException) thrown;
            } else if (thrown instanceof Error) {
                throw (Error) thrown;
            } else {
                throw e; 
            }
        } catch (RunSecurityManager.ExitCalledException e) {
            // XXX need to update run validator (a) to accept null result or (b) to require zero result, and set 0 if completed normally
            status.finish(new Integer(e.exitCode));
        } catch (ClassNotFoundException e) {
            String[] classes = FileUtil.listFiles(sandbox.classesDir);
            MessageUtil.info(status, "sandbox.classes: " + Arrays.asList(classes));
            MessageUtil.fail(status, null, e);
        } finally {
            if (null != oldOut) {
                System.setOut(oldOut);
            }
            if (null != oldErr) {
                System.setErr(oldErr);
            }
            RunSecurityManager.ME.releaseJavaRunThread(this);
            if (!completedNormally) {
                MessageUtil.info(status, "targetClass: " + targetClass);
                MessageUtil.info(status, "loader: " + loader);
            }
        }
        return passed;
    }
    
    /**
     * Run in another VM by grabbing Java, bootclasspath, classpath, etc.
     * This assumes any exception or output to System.err is a failure,
     * and any normal completion is a pass.
     * @param status
     * @param libs
     * @param dirs
     * @return
     */
    protected boolean runInOtherVM(IRunStatus status, File[] libs, File[] dirs) {
        // assert spec.fork || !LangUtil.isEmpty(spec.aspectpath);
        ArrayList cmd = new ArrayList();
        cmd.add(FileUtil.getBestPath(spec.forkSpec.java));
        if (!LangUtil.isEmpty(spec.forkSpec.vmargs)) {
            cmd.addAll(Arrays.asList(spec.forkSpec.vmargs));
        }
        final String classpath;
        {
            StringBuffer cp = new StringBuffer();
            appendClasspath(cp, spec.forkSpec.bootclasspath);
            appendClasspath(cp, dirs);        
            appendClasspath(cp, libs);
            File[] classpathFiles = sandbox.findFiles(spec.classpath);
            int cpLength = (null == classpathFiles ? 0 : classpathFiles.length);
            int spLength = (null == spec.classpath ? 0 : spec.classpath.length);
            if (cpLength != spLength) {
                throw new Error("unable to find " + Arrays.asList(spec.classpath)
                        + " got " + Arrays.asList(classpathFiles));
            }
            appendClasspath(cp, classpathFiles);
            File[] stdlibs = {Globals.F_aspectjrt_jar, Globals.F_testingclient_jar};
            appendClasspath(cp, stdlibs);
            classpath = cp.toString();
        }
        if (!spec.isLTW()) {
            cmd.add("-classpath");
            cmd.add(classpath);
        } else {
            // verify 1.4 or above, assuming same vm as running this
            if (!Globals.supportsJava("1.4")) {
                throw new Error("load-time weaving test requires Java 1.4+");
            }
            cmd.add("-Djava.system.class.loader=org.aspectj.weaver.WeavingURLClassLoader");
            // assume harness VM classpath has WeavingURLClassLoader (but not others)
            cmd.add("-classpath");
            cmd.add(System.getProperty("java.class.path"));

            File[] aspectJars = sandbox.findFiles(spec.aspectpath);
            if (aspectJars.length != spec.aspectpath.length) {
                throw new Error("unable to find " + Arrays.asList(spec.aspectpath));
            }
            StringBuffer cp = new StringBuffer();
            appendClasspath(cp, aspectJars);
            cmd.add("-Daj.aspect.path=" + cp.toString());
            cp.append(classpath); // appendClasspath writes trailing delimiter
            cmd.add("-Daj.class.path=" + cp.toString());
        }
        cmd.add(spec.className);
        cmd.addAll(spec.options);
        String[] command = (String[]) cmd.toArray(new String[0]);

        final IMessageHandler handler = status;
        // setup to run asynchronously, pipe streams through, and report errors
        class DoneFlag {
            boolean done;
            boolean failed;
            int code;
        }
        final StringBuffer commandLabel = new StringBuffer();
        final DoneFlag doneFlag = new DoneFlag();
        LangUtil.ProcessController controller
            = new LangUtil.ProcessController() {
                protected void doCompleting(Thrown ex, int result) {
                    if (!ex.thrown && (0 == result)) {
                        doneFlag.done = true;
                        return; // no errors
                    }
                    // handle errors
                    String context = spec.className 
                        + " command \"" 
                        + commandLabel 
                        + "\"";
                    if (null != ex.fromProcess) {
                        if (!expectedException(ex.fromProcess)) {
                            String m = "Exception running " + context;
                            MessageUtil.abort(handler, m, ex.fromProcess);
                            doneFlag.failed = true;
                        }
                    } else if (0 != result) {
                        doneFlag.code = result;
                    }
                    if (null != ex.fromInPipe) {
                        String m = "Error processing input pipe for " + context;
                        MessageUtil.abort(handler, m, ex.fromInPipe);
                        doneFlag.failed = true;
                    }
                    if (null != ex.fromOutPipe) {
                        String m = "Error processing output pipe for " + context;
                        MessageUtil.abort(handler, m, ex.fromOutPipe);
                        doneFlag.failed = true;
                    }
                    if (null != ex.fromErrPipe) {
                        String m = "Error processing error pipe for " + context;
                        MessageUtil.abort(handler, m, ex.fromErrPipe);
                        doneFlag.failed = true;
                    }
                    doneFlag.done = true;
                }
            };
        controller.init(command, spec.className);
        if (null != spec.forkSpec.javaHome) {
            controller.setEnvp(new String[] {"JAVA_HOME=" + spec.forkSpec.javaHome});
        }
        commandLabel.append(Arrays.asList(controller.getCommand()).toString());
        final ByteArrayOutputStream errSnoop 
            = new ByteArrayOutputStream();
        final ByteArrayOutputStream outSnoop 
            = new ByteArrayOutputStream();
        controller.setErrSnoop(errSnoop);
        controller.setOutSnoop(outSnoop);
        controller.start();
        // give it 3 minutes...
        long maxTime = System.currentTimeMillis() + 3 * 60 * 1000;
        boolean waitingForStop = false;
        while (!doneFlag.done) {
            if (maxTime < System.currentTimeMillis()) {
                if (waitingForStop) { // hit second timeout - bail
                    break;
                }
                MessageUtil.fail(status, "timeout waiting for process"); 
                doneFlag.failed = true;
                controller.stop(); 
                // wait 1 minute to evaluate results of stopping
                waitingForStop = true;
                maxTime = System.currentTimeMillis() + 1 * 60 * 1000;
            }
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                // ignore
            }
        }

        boolean foundException = false;
        if (0 < errSnoop.size()) {
            if (expectedException(errSnoop)) {
                foundException = true;
            } else if (spec.errStreamIsError) {
                MessageUtil.error(handler, errSnoop.toString());
                if (!doneFlag.failed) {
                    doneFlag.failed = true;
                } 
            } else {
                MessageUtil.info(handler, "Error stream: " + errSnoop.toString());
            }
        }
        if (0 < outSnoop.size()) {
            if (expectedException(outSnoop)) {
                foundException = true;
            } else if (spec.outStreamIsError) {
                MessageUtil.error(handler, outSnoop.toString());
                if (!doneFlag.failed) {
                    doneFlag.failed = true;
                } 
            } else {
                MessageUtil.info(handler, "Output stream: " + outSnoop.toString());
            }
        }
        if (!foundException) {
            if (null != spec.expectedException) {
                String m = " expected exception " + spec.expectedException;
                MessageUtil.fail(handler, m);                
                doneFlag.failed = true;
            } else if (0 != doneFlag.code) {
                String m = doneFlag.code + " result from " + commandLabel;
                MessageUtil.fail(handler, m);
                doneFlag.failed = true;
            }
        }
        if (doneFlag.failed) {
            MessageUtil.info(handler, "other-vm command-line: " + commandLabel);
        }
        return !doneFlag.failed;
    }

    protected boolean expectedException(Throwable thrown) {
        if (null != spec.expectedException) {
            String cname = thrown.getClass().getName();
            if (-1 != cname.indexOf(spec.expectedException)) {
                return true; // caller sets value for returns normally
            }
        }
        return false;
    }
    
    protected boolean expectedException(ByteArrayOutputStream bout) {
        return ((null != spec.expectedException)
                && (-1 != bout.toString().indexOf(spec.expectedException)));
    }
    
    /**
     * Clear (static) testing state and setup base directory,
     * unless spec.skipTesting.
     * @return null if successful, error message otherwise
     */
    protected void setupTester(File baseDir, ClassLoader loader, IMessageHandler handler) {
        if (null == loader) {
            setupTester(baseDir, handler);
            return;
        }
        File baseDirSet = null;
        try {
            if (!spec.skipTester) { 
                Class tc = loader.loadClass("org.aspectj.testing.Tester");
                // Tester.clear();
                Method m = tc.getMethod("clear", new Class[0]);
                m.invoke(null, new Object[0]);             
                // Tester.setMessageHandler(handler);
                m = tc.getMethod("setMessageHandler", new Class[] {IMessageHandler.class});
                m.invoke(null, new Object[] { handler});             
                
                //Tester.setBASEDIR(baseDir);
                m = tc.getMethod("setBASEDIR", new Class[] {File.class});
                m.invoke(null, new Object[] { baseDir});             

                //baseDirSet = Tester.getBASEDIR();
                m = tc.getMethod("getBASEDIR", new Class[0]);
                baseDirSet = (File) m.invoke(null, new Object[0]);             
                
                if (!baseDirSet.equals(baseDir)) {
                    String l = "AjcScript.setupTester() setting "
                             + baseDir + " returned " + baseDirSet;
                    MessageUtil.debug(handler, l);
                }
            }
        } catch (Throwable t) {
            MessageUtil.abort(handler, "baseDir=" + baseDir, t);
        }
    }

    /**
     * Clear (static) testing state and setup base directory,
     * unless spec.skipTesting.
     * This implementation assumes that Tester is defined for the
     * same class loader as this class.
     * @return null if successful, error message otherwise
     */
    protected void setupTester(File baseDir, IMessageHandler handler) {
        File baseDirSet = null;
        try {
            if (!spec.skipTester) {                
                Tester.clear();
                Tester.setMessageHandler(handler);
                Tester.setBASEDIR(baseDir);
                baseDirSet = Tester.getBASEDIR();
                if (!baseDirSet.equals(baseDir)) {
                    String l = "AjcScript.setupTester() setting "
                             + baseDir + " returned " + baseDirSet;
                    MessageUtil.debug(handler, l);
                }
            }
        } catch (Throwable t) {
            MessageUtil.abort(handler, "baseDir=" + baseDir, t);
        }
    }
	public String toString() {
        return "JavaRun(" + spec + ")";
	}
        
    /**
     * Struct class for fork attributes and initialization.
     * This supports defaults for forking using system properties
     * which will be overridden by any specification.
     * (It differs from CompilerRun, which supports option 
     * overriding by passing values as harness arguments.)
     */
    public static class ForkSpec {
        /**
         * key for system property for default value for forking
         * (true if set to true)
         */
        public static String FORK_KEY = "javarun.fork";
        public static String JAVA_KEY = "javarun.java";
        public static String VM_ARGS_KEY = "javarun.vmargs";
        public static String JAVA_HOME_KEY = "javarun.java.home";
        public static String BOOTCLASSPATH_KEY = "javarun.bootclasspath";
        static final ForkSpec FORK;
        static {
            ForkSpec fork  = new ForkSpec();
            fork.fork = Boolean.getBoolean(FORK_KEY);
            fork.java = getFile(JAVA_KEY);
            if (null == fork.java) {
                fork.java = LangUtil.getJavaExecutable();
            }
            fork.javaHome = getFile(JAVA_HOME_KEY);
            fork.bootclasspath = XMLWriter.unflattenList(getProperty(BOOTCLASSPATH_KEY));
            fork.vmargs = XMLWriter.unflattenList(getProperty(VM_ARGS_KEY));
            FORK = fork;
        }
        private static File getFile(String key) {
            String path = getProperty(key);
            if (null != path) {
                File result = new File(path);
                if (result.exists()) {
                    return result;
                }
            }
            return null;
        }
        private static String getProperty(String key) {
            try {
                return System.getProperty(key);
            } catch (Throwable t) {
                return null;
            }
        }
        private boolean fork;
        private String[] bootclasspath;
        private File java;
        private File javaHome;
        private String[] vmargs;

        private ForkSpec() {
            copy(FORK);
        }
        
        private void copy(ForkSpec forkSpec) {
            if (null != forkSpec) {
                fork = forkSpec.fork;
                bootclasspath = forkSpec.bootclasspath;
                java = forkSpec.java;
                javaHome = forkSpec.javaHome;
                vmargs = forkSpec.vmargs;
            }
        }

        /**
         * @return "" or bootclasspath with File.pathSeparator internal delimiters
         */
        String getBootclasspath() {
            if (LangUtil.isEmpty(bootclasspath)) {
                return "";
            }
            return FileUtil.flatten(bootclasspath, null);
        }
    }
    
    /** 
     * Initializer/factory for JavaRun.
     * The classpath is not here but precalculated in the Sandbox.
     */
    public static class Spec extends AbstractRunSpec {
        static {
            try {
                System.setSecurityManager(RunSecurityManager.ME);
            } catch (Throwable t) {
                System.err.println("JavaRun: Security manager set - no System.exit() protection");
            }
        }
        public static final String XMLNAME = "run";
        /**
         * skip description, skip sourceLocation, 
         * do keywords, do options, skip paths, do comment,
         * skip staging,   skip badInput,
         * do dirChanges, do messages but skip children. 
         */
        private static final XMLNames NAMES = new XMLNames(XMLNames.DEFAULT,
                "", "", null, null, "", null, "", "", false, false, true);
                
        /** fully-qualified name of the class to run */
        protected String className;
        
        /** minimum required version of Java, if any */
        protected String javaVersion;
        
        /** if true, skip Tester setup (e.g., if Tester n/a) */
        protected boolean skipTester;
        
        /** if true, report text to output stream as error */
        protected boolean outStreamIsError;

        /** if true, report text to error stream as error */
        protected boolean errStreamIsError = true;
        
        protected final ForkSpec forkSpec;
        protected String[] aspectpath;
        protected boolean useLTW;
        protected String[] classpath;
        protected String expectedException;
        
        public Spec() {
            super(XMLNAME);
            setXMLNames(NAMES);
            forkSpec = new ForkSpec();
        }
        
        protected void initClone(Spec spec) 
                throws CloneNotSupportedException {
            super.initClone(spec);
            spec.className = className;            
            spec.errStreamIsError = errStreamIsError;
            spec.javaVersion = javaVersion;
            spec.outStreamIsError = outStreamIsError;
            spec.skipTester = skipTester;
            spec.forkSpec.copy(forkSpec);
        }
        
        public Object clone() throws CloneNotSupportedException {
            Spec result = new Spec();
            initClone(result);
            return result;    
        }

        public boolean isLTW() {
            return useLTW || (null != aspectpath);
        }
        
        /**
         * @param version "1.1", "1.2", "1.3", "1.4"
         * @throws IllegalArgumentException if version is not recognized
         */
        public void setJavaVersion(String version) {
        	Globals.supportsJava(version);
            this.javaVersion = version;
        }
        
        /** @className fully-qualified name of the class to run */
        public void setClassName(String className) {
            this.className = className;
        }
        
        public void setLTW(String ltw) {
            useLTW = TestUtil.parseBoolean(ltw);
        }
        
        public void setAspectpath(String path) {
            this.aspectpath = XMLWriter.unflattenList(path);
        }
        public void setException(String exception) {
            this.expectedException = exception;
        }
        
        public void setClasspath(String path) {
            this.classpath = XMLWriter.unflattenList(path);
        }
        public void setErrStreamIsError(String errStreamIsError) {
            this.errStreamIsError = TestUtil.parseBoolean(errStreamIsError);
        }

        public void setOutStreamIsError(String outStreamIsError) {
            this.outStreamIsError = TestUtil.parseBoolean(outStreamIsError);
        }

        /** @param skip if true, then do not set up Tester */
        public void setSkipTester(boolean skip) {
            skipTester = skip;
        }
        
        public void setFork(boolean fork) {
            forkSpec.fork = fork;
        }
        
        /**
         * @param vmargs comma-delimited list of arguments for java,
         * typically -Dname=value,-DanotherName="another value"
         */
        public void setVmArgs(String vmargs) {
            forkSpec.vmargs = XMLWriter.unflattenList(vmargs);
        }
        
        /** override to set dirToken to Sandbox.RUN_DIR */
        public void addDirChanges(DirChanges.Spec spec) {
            if (null == spec) {
                return;
            }
            spec.setDirToken(Sandbox.RUN_DIR);
            super.addDirChanges(spec);
        }

        /** @return a JavaRun with this as spec if setup completes successfully. */
        public IRunIterator makeRunIterator(Sandbox sandbox, Validator validator) {
            JavaRun run = new JavaRun(this);
            if (run.setupAjcRun(sandbox, validator)) {
                // XXX need name for JavaRun
                return new WrappedRunIterator(this, run);
            }
            return null;
        }

        /** 
         * Write this out as a run element as defined in
         * AjcSpecXmlReader.DOCTYPE.
         * @see AjcSpecXmlReader#DOCTYPE 
         * @see IXmlWritable#writeXml(XMLWriter) 
         */
        public void writeXml(XMLWriter out) {
            String attr = XMLWriter.makeAttribute("class", className);
            out.startElement(xmlElementName, attr, false);
            if (skipTester) {
                out.printAttribute("skipTester", "true");
            }
            if (null != javaVersion) {
                out.printAttribute("vm", javaVersion);
            }
            if (outStreamIsError) {
                out.printAttribute("outStreamIsError", "true");
            }
            if (!errStreamIsError) { // defaults to true
                out.printAttribute("errStreamIsError", "false");
            }
            super.writeAttributes(out);
            out.endAttributes();
            if (!LangUtil.isEmpty(dirChanges)) {
                DirChanges.Spec.writeXml(out, dirChanges);
            }
            SoftMessage.writeXml(out, getMessages());
            out.endElement(xmlElementName);
        }
        public String toLongString() {
            return toString() + "[" + super.toLongString() + "]";        
        }
        
        public String toString() {
            if (skipTester) {
                return "JavaRun(" + className + ", skipTester)";
            } else {
                return "JavaRun(" + className + ")";
            }
        }
        
        /**
         * This implementation skips if:
         * <ul>
         * <li>current VM is not at least any specified javaVersion </li>
         * </ul>
         * @return false if this wants to be skipped, true otherwise
         */
        protected boolean doAdoptParentValues(RT parentRuntime, IMessageHandler handler) {
            if (!super.doAdoptParentValues(parentRuntime, handler)) {
                return false;
            }
            if ((null != javaVersion) && (!Globals.supportsJava(javaVersion))) {
                skipMessage(handler, "requires Java version " + javaVersion);
                return false;
            }
            return true;
        }  
     }
    /**
     * This permits everything but System.exit() in the context of a 
     * thread set by JavaRun.
     * XXX need to update for thread spawned by that thread
     * XXX need to update for awt thread use after AJDE wrapper doesn't
     */
    public static class RunSecurityManager extends SecurityManager {
        public static RunSecurityManager ME = new RunSecurityManager();
        private Thread runThread;
        private RunSecurityManager(){}
        private synchronized void setJavaRunThread(JavaRun run) {
            LangUtil.throwIaxIfNull(run, "run");
            runThread = Thread.currentThread();
        }
        private synchronized void releaseJavaRunThread(JavaRun run) {
            LangUtil.throwIaxIfNull(run, "run");
            runThread = null;
        }
        /** @throws ExitCalledException if called from the JavaRun-set thread */
        public void checkExit(int exitCode) throws ExitCalledException {
            if ((null != runThread) && runThread.equals(Thread.currentThread())) {
                throw new ExitCalledException(exitCode);
            }
        }
        public void checkAwtEventQueueAccess() {
            if ((null != runThread) && runThread.equals(Thread.currentThread())) {
                throw new AwtUsedException();
            }
        }
        public void checkSystemClipboardAccess() {
            // permit
        }
        // used by constrained calls
        public static class ExitCalledException extends SecurityException {
            public final int exitCode;
            public ExitCalledException(int exitCode) {
                this.exitCode = exitCode;
            }
        }
        public static class AwtUsedException extends SecurityException {
            public AwtUsedException() { }
        }
        // permit everything else
        public void checkAccept(String arg0, int arg1) {
        }
        public void checkAccess(Thread arg0) {
        }
        public void checkAccess(ThreadGroup arg0) {
        }
        public void checkConnect(String arg0, int arg1) {
        }
        public void checkConnect(String arg0, int arg1, Object arg2) {
        }
        public void checkCreateClassLoader() {
        }
        public void checkDelete(String arg0) {
        }
        public void checkExec(String arg0) {
        }
        public void checkLink(String arg0) {
        }
        public void checkListen(int arg0) {
        }
        public void checkMemberAccess(Class arg0, int arg1) {
        }
        public void checkMulticast(InetAddress arg0) {
        }
        public void checkMulticast(InetAddress arg0, byte arg1) {
        }
        public void checkPackageAccess(String arg0) {
        }
        public void checkPackageDefinition(String arg0) {
        }
        public void checkPermission(Permission arg0) {
        }
        public void checkPermission(Permission arg0, Object arg1) {
        }
        public void checkPrintJobAccess() {
        }
        public void checkPropertiesAccess() {
        }
        public void checkPropertyAccess(String arg0) {
        }
        public void checkRead(FileDescriptor arg0) {
        }
        public void checkRead(String arg0) {
        }
        public void checkRead(String arg0, Object arg1) {
        }
        public void checkSecurityAccess(String arg0) {
        }
        public void checkSetFactory() {
        }
        public boolean checkTopLevelWindow(Object arg0) {
            return true;
        }
        public void checkWrite(FileDescriptor arg0) {
        }
        public void checkWrite(String arg0) {
        }
        
    }

}
