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

package org.aspectj.testing.harness.bridge;

import java.io.File;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.StringTokenizer;

import org.aspectj.bridge.*;
import org.aspectj.bridge.IMessage;
import org.aspectj.bridge.IMessageHandler;
import org.aspectj.bridge.ISourceLocation;
import org.aspectj.bridge.MessageHandler;
import org.aspectj.bridge.MessageUtil;
import org.aspectj.testing.run.IRunIterator;
//import org.aspectj.testing.util.*;
import org.aspectj.testing.util.BridgeUtil;
import org.aspectj.testing.util.options.*;
import org.aspectj.testing.util.options.Option.InvalidInputException;
import org.aspectj.testing.xml.IXmlWritable;
import org.aspectj.testing.xml.SoftMessage;
import org.aspectj.testing.xml.XMLWriter;
import org.aspectj.util.LangUtil;

/**
 * Base class for initialization of components expecting messages,
 * options, files/paths, and source locations (resolved files),
 * and potentially containing child Spec.
 * <p>
 * <u>initialization</u>: This defines bean/xml setters for all.
 * This converts String to IMessage using
 * {@link MessageUtil#readMessage(String)}
 * and String to ISourceLocation using 
 * {@link BridgeUtil#makeSourceLocation(input)}.
 * See those APIs for input form and limitations.
 * A Spec also accepts (or rejects) runtime configuration from a parent
 * in {@link adoptParentValues(RT, IMessageHandler)}.  
 * Since some children Spec may balk but this parent Spec continue,
 * use {@link getChildren()} to get the full list of children Spec,
 * but {@link getWorkingChildren()} to get the list of children that
 * are not being skipped in accordance with runtime configuration.
 * <p> 
 * <u>subclassing</u>: subclasses wishing other than
 * the default behavior for reading String input should override the
 * corresponding (bean) setter.  They can also override the 
 * add{foo} methods to get notice or modify objects constructed
 * from the input.
 * <p>
 * <u>bean properties</u>: because this is designed to work
 * by standard Java bean introspection, take care to follow
 * bean rules when adding methods.   In particular, a property
 * is illegal if the setter takes a different type than the
 * getter returns.  That means, e.g., that all List and array[] getters
 * should be named "get{property}[List|Array]".  Otherwise 
 * the XML readers will silently fail to set the property
 * (perhaps with trace information that the property had
 * no write method or was read-only).
 * <p>
 * <u>Coordination with writers</u>: because this reads the contents
 * of values written by IXmlWritable, they should ensure their
 * values are readable.  When flattening and unflattening
 * lists, the convention is to use the {un}flattenList(..) methods
 * in XMLWriter.
 * @see XMLWriter@unflattenList(String)
 * @see XMLWriter@flattenList(List)
 */
abstract public class AbstractRunSpec implements IRunSpec { // XXX use MessageHandler?


	private static final Fork FORK;

	static {
		String value = 
			Globals.getSystemProperty(Globals.FORK_NAME, null);
		FORK = (LangUtil.isEmpty(value) 
			? Fork.NOFORK 
			: new Fork(value));
	}
    /** true if we expect to use a staging directory */
    boolean isStaging;
    
    /** true if this spec permits bad input (e.g., to test error handling) */
    boolean badInput;  
    
    protected String description;
    
    /** optional source location of the specification itself */
    protected ISourceLocation sourceLocation;
    
    private BitSet skipSet;
    private boolean skipAll;
    
    protected String xmlElementName; // nonfinal only for clone()
    protected final ArrayList /*String*/ keywords; 
    protected final IMessageHolder /*IMessage*/ messages;
    protected final ArrayList /*String*/ options;
    protected final ArrayList /*String*/ paths;
    protected final ArrayList /*ISourceLocation*/ sourceLocations; // XXX remove?
    protected final ArrayList /*IRunSpec*/ children;
    protected final ArrayList /*DirChanges.Spec*/ dirChanges;
    protected XMLNames xmlNames;
    protected String comment;
    
    /** These options are 1:1 with spec, but set at runtime (not saved) */
    public final RT runtime;

    /** if true, then any child skip causes this to skip */
	protected boolean skipIfAnyChildSkipped; // nonfinal only for cloning

    public AbstractRunSpec(String xmlElementName) {
        this(xmlElementName, true);
    }
    
    public AbstractRunSpec(String xmlElementName, boolean skipIfAnyChildSkipped) {
        if (null == xmlElementName) {
            xmlElementName = "spec";
        }
        this.xmlElementName = xmlElementName;
        messages = new MessageHandler(true);
        options = new ArrayList();
        paths = new ArrayList();
        sourceLocations = new ArrayList();
        keywords = new ArrayList();
        children = new ArrayList();
        dirChanges = new ArrayList();
        xmlNames = XMLNames.DEFAULT;
        runtime = new RT();
        this.skipIfAnyChildSkipped = skipIfAnyChildSkipped;
    }    
    

    /** @param comment ignored if null */
    public void setComment(String comment) {
        if (!LangUtil.isEmpty(comment)) {
            this.comment = comment;
        }
    }
    
    public void setStaging(boolean staging) {
        isStaging = staging;
    }
    
    public void setBadInput(boolean badInput) {
        this.badInput = badInput;
    }

    boolean isStaging() {
        return isStaging;
    }
        
    // ------- description (title, label...)    
    public void setDescription(String description) {
        this.description = description;
    }

    public String getDescription() {
        return description;
    }
    // ------- source location of the spec
    
    public void setSourceLocation(ISourceLocation sourceLocation) {
        this.sourceLocation = sourceLocation;
    }

    public ISourceLocation getSourceLocation() {
        return sourceLocation;
    }
     
    // ------- keywords
    /** @param keyword added after trimming if not empty */
    public void setKeyword(String keyword) {
        addKeyword(keyword);
    }
    
    /** Add keyword if non-empty and not duplicate */
    public void addKeyword(String keyword) {
        if (!LangUtil.isEmptyTrimmed(keyword)) {
            keyword = keyword.trim();
            if (!keywords.contains(keyword)) {
                keywords.add(keyword);
            }
        }
    }
    public void setKeywords(String items) {
        addKeywords(items);
    }
    
    public void addKeywords(String items) {
        if (null != items) {
            addKeywords(XMLWriter.unflattenList(items));
        }    
    }
    public void addKeywords(String[] ra) {
        if (null != ra) {
            for (int i = 0; i < ra.length; i++) {
                addKeyword(ra[i]);
            }
        }    
    }
    public ArrayList getKeywordsList() {
        return makeList(keywords);
    }

   // ------- options - String args

    /** @return ArrayList of String options */    
    public ArrayList getOptionsList() {
        return makeList(options);
    }
    
    /** @return String[] of options */    
    public String[] getOptionsArray() {
        return (String[]) options.toArray(new String[0]);
    }
    
    public void setOption(String option) {
        addOption(option);
    }
    
    public void addOption(String option) {
        if ((null != option) && (0 < option.length())) {
            options.add(option);
        }    
    }
    
    /** add options (from XML/bean) - removes any existing options */    
    public void setOptions(String items) {
        this.options.clear();
        addOptions(items);
    }
    
    /** 
     * Set options, removing any existing options.
     * @param options String[] options to use - may be null or empty
     */
    public void setOptionsArray(String[] options) {
        this.options.clear();
        if (!LangUtil.isEmpty(options)) {
            this.options.addAll(Arrays.asList(options));
        }
    }
    
    public void addOptions(String items) {
        if (null != items) {
            addOptions(XMLWriter.unflattenList(items));
        }    
    }
    
    public void addOptions(String[] ra) {
        if (null != ra) {
            for (int i = 0; i < ra.length; i++) {
                addOption(ra[i]);
            }
        }    
    }

    // -------------- source locations
//    /** @return ArrayList of ISourceLocation sourceLocs */    
//    public ArrayList getSourceLocationsList() {
//        return makeList(sourceLocations);
//    }
//
//    /** @return ISourceLocation[] sourceLocs */    
//    public ISourceLocation[] getSourceLocationsArray() {
//        return (ISourceLocation[]) sourceLocations.toArray(new ISourceLocation[0]);
//    }
//
//    
//    public void setSourceLocation(String input) {
//        if (null != input) {
//            ISourceLocation sl = BridgeUtil.makeSourceLocation(input);
//            if (null != sl) {
//                addSourceLocation(sl);
//            }
//            // XXX need error-handling for bad input            
//        }
//    }
//    
//    public void addSourceLocation(ISourceLocation sourceLoc) {
//        if (null != sourceLoc) {
//            sourceLocations.add(sourceLoc);
//        }    
//    }

    // --------------- (String) paths
    /** @return ArrayList of String paths */    
    public ArrayList getPathsList() {
        return makeList(paths);
    }
    
    /** @return String[] of paths */    
    public String[] getPathsArray() {
        return (String[]) paths.toArray(new String[0]);
    }

//    /** @return String[] of paths, removing any matching stripPrefix prefix */    
//    public String[] getPathsArray(String stripPrefix) {
//        String[] result = getPathsArray();
//        if (!LangUtil.isEmpty(stripPrefix)) {
//            final int LEN = stripPrefix.length();
//            for (int i = 0; i < result.length; i++) {
//                if ((null != result[i]) && result[i].startsWith(stripPrefix)) {
//                    result[i] = result[i].substring(LEN);
//                }               
//            }
//        }
//        return result;
//    }
//
//    /** @return ArrayList of File baseDir/{path} */    
//    public ArrayList getPathsAsFile(File baseDir) {
//        if (null == baseDir) {
//            baseDir = new File(".");
//        }
//        ArrayList result = makeList(null);
//        for (Iterator iter = paths.iterator(); iter.hasNext();) {
//            String path = (String) iter.next();
//            result.add(new File(baseDir, path));
//        }
//        return result;
//    }
    
    public void setPath(String path) {
        addPath(path);
    }

    public void setPaths(String paths) {
        addPaths(paths);
    }
    
    public void addPath(String path) {
        if (null != path) {
            paths.add(path);
        }    
    }

    public void addPaths(String items) {
        if (null != items) {
            addPaths(XMLWriter.unflattenList(items));
        }    
    }

    public void addPaths(String[] ra) {
        if (null != ra) {
            for (int i = 0; i < ra.length; i++) {
                addPath(ra[i]);
            }
        }    
    }

    public void addWrapFile(WrapFile file) {
        if (null != file) {
            paths.add(file.path);
        }    
    }
    
    // --------------------- dir changes
    public void addDirChanges(DirChanges.Spec dirChangesSpec) {
        if (null != dirChangesSpec) {
            dirChanges.add(dirChangesSpec);
        }
    }
    
    // --------------------- messages
    public void setMessage(String message) {
        addMessage(message);
    }

    public void addMessage(IMessage message) {
        if (null != message) {
            if (!messages.handleMessage(message)) {
                String s = "invalid message: " + message;
                throw new IllegalArgumentException(s);
            }
        }
    }
    
    public void addMessage(String message) {
        if (null != message) {
            addMessage(BridgeUtil.readMessage(message));
        }
    }
    
    /** this can ONLY work if each item has no internal comma 
     */
    public void addMessages(String items) {
        if (null != items) {
            String[] ra = XMLWriter.unflattenList(items);
            for (int i = 0; i < ra.length; i++) {
                addMessage(ra[i]);
            }
        }    
    }
    public void addMessages(List messages) {
        if (null != messages) {
            for (Iterator iter = messages.iterator(); iter.hasNext();) {
				Object o = iter.next();
				if (o instanceof IMessage) {
                    addMessage((IMessage) o);
                } else {
                    // XXX warning?
                }
			}
        }    
    }

    /** @return int number of message of this kind (optionally or greater */
    public int numMessages(IMessage.Kind kind, boolean orGreater) {
        return messages.numMessages(kind, orGreater);
    }
    
    public IMessageHolder getMessages() {
        return messages;
    }
    
    
    public void addChild(IRunSpec child) {
        // fyi, child is added when complete (depth-first), not when initialized,
        // so cannot affect initialization of child here
        if (null != child) {
            children.add(child); 
        }
    }
    
    /** @return copy of children list */
    public ArrayList getChildren() {
        return makeList(children);
    }

    /** @return copy of children list without children to skip */
    public ArrayList getWorkingChildren() {
        if (skipAll) {
            return new ArrayList();
        }
        if (null == skipSet) {
            return getChildren();
        }
        ArrayList result = new ArrayList();
        int i = 0;
        for (Iterator iter = children.listIterator(); 
            iter.hasNext(); i++) {
            Object child = iter.next();
		    if (!skipSet.get(i)) {
                result.add(child);
            }
		}
        return result;
    }

    /** 
     * Recursively absorb parent values if different.
     * This implementation calls doAdoptParentValues(..)
     * and then calls this for any children.  
     * This is when skipped children are determined.
     * Children may elect to balk at this point, reducing the
     * number of children or causing this spec to skip if
     * skipIfAnyChildrenSkipped.  For each test skipped, either
     * this doAdoptParentValues(..) or the child's adoptParentValues(..)
     * should add one info message with the reason this is being skipped.
     * The only reason to override this would be to NOT
     * invoke the same for children, or to do something similar
     * for children which are not AbstractRunSpec.
     * @param parentRuntime the RT values to adopt - ignored if null
     * @param handler the IMessageHandler for info messages when skipping
     * @return false if this wants to be skipped, true otherwise
     */
    public  boolean adoptParentValues(RT parentRuntime, IMessageHandler handler) {
        boolean skipped = false;
        skipAll = false;
        skipSet = new BitSet();
        if (null != parentRuntime) {
            skipped = !doAdoptParentValues(parentRuntime, handler);
            if (skipped && skipIfAnyChildSkipped) { // no need to continue checking
                skipAll = true;
                return false;
            }
            int i = 0;
            for (ListIterator iter = children.listIterator(); iter.hasNext(); i++) {
				IRunSpec child = (IRunSpec) iter.next();
				if (child instanceof AbstractRunSpec) { // XXX ugly instanceof
                    AbstractRunSpec arsChild = (AbstractRunSpec) child;
                    if (!arsChild.adoptParentValues(runtime, handler)) {
                        skipSet.set(i);
                        //iter.remove();
                        if (!skipped) {
                            skipped = true;
                            if (skipIfAnyChildSkipped) { // no need to continue checking
//                                String m = "skipping " + toString() + " because child " 
//                                    + arsChild + " skipped";
//                                MessageUtil.info(handler, m);
                                skipAll = true;
                                //children.clear();
                                return false;
                            }
                        }
                    }
                }
			}
        }
        return true;
    }
    
    /** 
     * Adopt parent values.
     * This implementation makes a local copy.
     * If we interpret (and absorb) any options, they should be removed
     * from parentRuntime.
     * This sets verbose if different (override)
     * and directly adopts parentOptions if ours is null
     * and otherwise adds any non-null options we don't already have.
     * setting verbose and adding to parent options.
     * Implementors  override this to affect how parent values are adopted. 
     * Implementors should not recurse into children.
     * This method may be called multiple times, so implementors 
     * should not destroy any spec information.
     * Always add an info message when returning false to skip
     * @param parentRuntime the RT values to adopt - never null
     * @return false if this wants to be skipped, true otherwise
     */
    protected boolean doAdoptParentValues(RT parentRuntime, IMessageHandler handler) {
        if (runtime.verbose != parentRuntime.verbose) {
            runtime.verbose = parentRuntime.verbose;
        }
        if (!LangUtil.isEmpty(runtime.parentOptions)) {
            runtime.parentOptions.clear();
        }
        if (!LangUtil.isEmpty(parentRuntime.parentOptions)) {
            runtime.parentOptions.addAll(parentRuntime.parentOptions);
        }
        return true;
    }  
    
    /** 
     * Implementations call this when signalling skips to ensure consistency
     * in message formatting
     * @param handler the IMessageHandler sink - not null
     * @param reason the String reason to skip - not null
     */
    protected void skipMessage(IMessageHandler handler, String reason) {
        LangUtil.throwIaxIfNull(handler, "handler");
        LangUtil.throwIaxIfNull(handler, "reason");
        // XXX for Runs, label does not identify the test 
        String label = toString();
        MessageUtil.info(handler, "skipping \"" + label + "\" because " + reason);
    }
    
    // --------------------------- writing xml - would prefer castor..
    
    /**
     * Control XML output by renaming or suppressing output for
     * attributes and subelements.
     * Subelements are skipped by setting the XMLNames booleans to false.
     * Attributes are skipped by setting their name to null.
     * @param names XMLNames with new names and/or suppress flags.
     */
    protected void setXMLNames(XMLNames names) {
        if (null != names) {
            xmlNames = names;
        }
    }
    
//    /** @return null if value is null or name="{value}" otherwise */
//    private String makeAttr(XMLWriter out, String name, String value) {
//        if (null == value) {
//            return null;
//        }
//        return XMLWriter.makeAttribute(name, value);
//    }
//    
//    /** @return null if list is null or empty or name="{flattenedList}" otherwise */
//    private String makeAttr(XMLWriter out, String name, List list) {
//        if (LangUtil.isEmpty(list)) {
//            return null;
//        }
//        String flat = XMLWriter.flattenList(list);
//        return XMLWriter.makeAttribute(name, flat);
//    }
//    
    /** @return true if writeAttributes(..) will produce any output */
    protected boolean haveAttributes() {
        return ((!LangUtil.isEmpty(xmlNames.descriptionName)
                    && !LangUtil.isEmpty(description))
               || (!LangUtil.isEmpty(xmlNames.keywordsName)
                    && !LangUtil.isEmpty(keywords))                    
               || (!LangUtil.isEmpty(xmlNames.optionsName)
                    && !LangUtil.isEmpty(options)) 
               || (!LangUtil.isEmpty(xmlNames.pathsName)
                    && !LangUtil.isEmpty(paths)));
    }
    
    /**
     * Write attributes without opening or closing elements/attributes.
     * An attribute is written only if the value is not empty
     * and the name in xmlNames is not empty
     */
    protected void writeAttributes(XMLWriter out) {
        if (!LangUtil.isEmpty(xmlNames.descriptionName)
            && !LangUtil.isEmpty(description)) {
            out.printAttribute(xmlNames.descriptionName, description);
        }
        if (!LangUtil.isEmpty(xmlNames.keywordsName)
            && !LangUtil.isEmpty(keywords)) {
            out.printAttribute(xmlNames.keywordsName, 
            XMLWriter.flattenList(keywords));
        }
        if (!LangUtil.isEmpty(xmlNames.optionsName)
            && !LangUtil.isEmpty(options)) {
            out.printAttribute(xmlNames.optionsName, XMLWriter.flattenList(options));
        }
        if (!LangUtil.isEmpty(xmlNames.pathsName)
            && !LangUtil.isEmpty(paths)) {
            out.printAttribute(xmlNames.pathsName, XMLWriter.flattenList(paths));
        }
        if (!LangUtil.isEmpty(xmlNames.commentName)
            && !LangUtil.isEmpty(comment)) {
            out.printAttribute(xmlNames.commentName, comment);
        }
        if (isStaging && !LangUtil.isEmpty(xmlNames.stagingName)) {
            out.printAttribute(xmlNames.stagingName, "true");
        }
        if (badInput && !LangUtil.isEmpty(xmlNames.badInputName)) {
            out.printAttribute(xmlNames.badInputName, "true");
        }
    }

    /**
     * The default implementation writes everything as attributes,
     * then subelements for dirChanges, messages, then subelements for children.
     * Subclasses that override may delegate back for any of these.
     * Subclasses may also set XMLNames to name or suppress any attribute
     * or subelement.
     * @see writeMessages(XMLWriter)
     * @see writeChildren(XMLWriter)
     * @see IXmlWritable#writeXml(XMLWriter) 
     */
    public void writeXml(XMLWriter out) {
        out.startElement(xmlElementName,false);
        writeAttributes(out);
        out.endAttributes();
        if (!xmlNames.skipMessages) {
            writeMessages(out);
        }
        if (!xmlNames.skipChildren) {
            writeChildren(out);            
        }
        out.endElement(xmlElementName);
    }

	/**
	 * Write messages.  Assumes attributes are closed,
     * can write child elements of current element.
	 */
	protected void writeMessages(XMLWriter out) {
        if (0 < messages.numMessages(null, true)) {
            SoftMessage.writeXml(out, messages);
        }

	}

    /**
     * Write children.  Assumes attributes are closed,
     * can write child elements of current element.
     */
    protected void writeChildren(XMLWriter out) {
        if (0 < children.size()) {
            for (Iterator iter = children.iterator(); iter.hasNext();) {
                IXmlWritable self = (IXmlWritable) iter.next();
                self.writeXml(out);
            }
        }
    }

    // --------------------------- logging
    
    public void printAll(PrintStream out, String prefix) {
        out.println(prefix + toString());
        for (Iterator iter = children.iterator(); iter.hasNext();) {
			AbstractRunSpec child = (AbstractRunSpec) iter.next(); // IRunSpec
            child.printAll(out, prefix + "    ");			
		}
    }
    
    /**
     * default implementation returns the description if not empty
     * or the unqualified class name otherwise.
     * Subclasses should not call toString from here unless they reimplement it.
     * @return name of this thing or type 
     */
    protected String getPrintName() {
        if (!LangUtil.isEmpty(description)) {
            return description;
        } else {
            return LangUtil.unqualifiedClassName(this);
        }
    }
    
    /** @return summary count of spec elements */
    public String toString() {        
        return getPrintName() + "(" + containedSummary() + ")"; 
    }
    
    /** @return String of the form (# [options|paths|locations|messages]).. */
    protected String containedSummary() {
        StringBuffer result = new StringBuffer();
        addListCount("options", options, result);
        addListCount("paths", paths, result);
        addListCount("sourceLocations", sourceLocations, result);
        List messagesList = messages.getUnmodifiableListView();
        addListCount("messages", messagesList, result);
        
        return result.toString().trim();
    }

    
    public String toLongString() { 
        String mssg = "";
        if (0 < messages.numMessages(null, true)) {
            mssg = " expected messages (" + MessageUtil.renderCounts(messages) + ")";
        }
        return getPrintName() + containedToLongString() + mssg.trim();
    }
    
    /** @return String of the form (# [options|paths|locations|messages]).. */
    protected String containedToLongString() {
        StringBuffer result = new StringBuffer();
        addListEntries("options", options, result);
        addListEntries("paths", paths, result);
        addListEntries("sourceLocations", sourceLocations, result);
        List messagesList = messages.getUnmodifiableListView();
        addListEntries("messages", messagesList, result);
        
        return result.toString();
    }

    protected void initClone(AbstractRunSpec spec) 
            throws CloneNotSupportedException {
        /*
         * clone associated objects only if not (used as?) read-only.
         */
        spec.badInput = badInput;
        spec.children.clear();
        for (Iterator iter = children.iterator(); iter.hasNext();) {
            // clone these...
            IRunSpec child = (IRunSpec) iter.next();
            // require all child classes to support clone?
            if (child instanceof AbstractRunSpec) {
                spec.addChild((AbstractRunSpec) ((AbstractRunSpec) child).clone());
            } else {
                throw new Error("unable to clone " + child);
            }
        }
        spec.comment = comment;
        spec.description = description;
        spec.dirChanges.clear();
        spec.dirChanges.addAll(dirChanges);
        spec.isStaging = spec.isStaging;
        spec.keywords.clear();
        spec.keywords.addAll(keywords);
        spec.messages.clearMessages();
        MessageUtil.handleAll(spec.messages, messages, false);
        spec.options.clear();
        spec.options.addAll(options);
        spec.paths.clear();
        spec.paths.addAll(paths);
        spec.runtime.copy(runtime);
        spec.skipAll = skipAll;
        spec.skipIfAnyChildSkipped = skipIfAnyChildSkipped;
        if (null != skipSet) {
            spec.skipSet = new BitSet();
            spec.skipSet.or(skipSet);
        }
        spec.sourceLocation = sourceLocation;
        spec.sourceLocations.clear();
        spec.sourceLocations.addAll(sourceLocations);
        spec.xmlElementName = xmlElementName;
        spec.xmlNames = ((AbstractRunSpec.XMLNames) xmlNames.clone());
    }

	protected final Fork getFork() {
		// spec ignored now, but perhaps not later...
		return FORK;
	}     

    private static void addListCount(String name, List list, StringBuffer sink) {
        int size = list.size();
        if ((null != list) && (0 < size)) {
            sink.append(" " + size + " ");
            sink.append(name);
        }
    }

    private static void addListEntries(String name, List list, StringBuffer sink) {
        if ((null != list) && (0 < list.size())) {
            sink.append(" " + list.size() + " ");
            sink.append(name);
            sink.append(": ");
            sink.append(list.toString());
        }
    }
    private ArrayList makeList(List list) {
        ArrayList result = new ArrayList();
        if (null != list) {
            result.addAll(list);
        }
        return result;
    }
    
    /** 
     * Subclasses use this to rename attributes or omit attributes or subelements.
     * To suppress output of an attribute, * pass "" as the name of the attribute. 
     * To use default entries, pass null for that entry.
     */
    public static class XMLNames {
        public static final XMLNames DEFAULT =
            new XMLNames(null, "description", "sourceLocation", 
                    "keywords", "options", "paths", "comment", 
                    "staging", "badInput", false, false, false);
        final String descriptionName;
        final String sourceLocationName;
        final String keywordsName;
        final String optionsName;
        final String pathsName;
        final String commentName;
        final String stagingName;
        final String badInputName;
        final boolean skipDirChanges;
        final boolean skipMessages;
        final boolean skipChildren;
        protected Object clone() {
            return new XMLNames(
                null,
                descriptionName,
                sourceLocationName,
                keywordsName,
                optionsName,
                pathsName,
                commentName,
                stagingName,
                badInputName,
                skipDirChanges,
                skipMessages,
                skipChildren);
        }
        
        // not runtime, skipAll, skipIfAnyChildSkipped, skipSet
        //     sourceLocations
        /** reset all names/behavior or pass defaultNames 
         * as the defaults for any null elements 
         */
        XMLNames(XMLNames defaultNames,
                String descriptionName,
                String sourceLocationName,
                String keywordsName,
                String optionsName,
                String pathsName,
                String commentName,
                String stagingName,
                String badInputName,
                boolean skipDirChanges,
                boolean skipMessages,
                boolean skipChildren) {
            this.skipDirChanges = skipDirChanges;
            this.skipMessages = skipMessages;
            this.skipChildren = skipChildren;
            if (null != defaultNames) {
                this.descriptionName = (null != descriptionName? descriptionName : defaultNames.descriptionName);
                this.sourceLocationName = (null != sourceLocationName ? sourceLocationName : defaultNames.sourceLocationName);
                this.keywordsName = (null != keywordsName ? keywordsName : defaultNames.keywordsName);
                this.optionsName = (null != optionsName ? optionsName : defaultNames.optionsName);
                this.pathsName = (null != pathsName ? pathsName : defaultNames.pathsName);
                this.commentName = (null != commentName ? commentName : defaultNames.commentName);
                this.stagingName = (null != stagingName ? stagingName : defaultNames.stagingName);
                this.badInputName = (null != badInputName ? badInputName : defaultNames.badInputName);
            } else {
                this.descriptionName = descriptionName;
                this.sourceLocationName = sourceLocationName;
                this.keywordsName = keywordsName;
                this.optionsName = optionsName;
                this.pathsName = pathsName;
                this.commentName = commentName;
                this.stagingName = stagingName;
                this.badInputName = badInputName;
            }
        }
    }

    
    /** subclasses implement this to create and set up a run */
    abstract public IRunIterator makeRunIterator(Sandbox sandbox, Validator validator);

    /** This is for separate file (sub-) elements with path attributes */
    public static class WrapFile {
        public String path;
        public void setPath(String path) {
            this.path = path;
        }
    }
    
    /** segregate runtime-only state in spec */
    public static class RT {        
        /** true if we should emit verbose messages */
        private boolean verbose;
        
        /** null unless parent set options for children to consider */
        final private ArrayList /*String*/ parentOptions;
        
        public RT() {
            parentOptions = new ArrayList();
        }
        
        public boolean isVerbose() {
            return verbose;
        }
        
        /** 
         * Set parent options - old options destroyed.
         * Will result in duplicates if duplicates added.
         * Null or empty entries are ignored
         * @param options ignored if null or empty
         */
        public void setOptions(String[] options) {
            parentOptions.clear();
            if (!LangUtil.isEmpty(options)) {
                for (int i = 0; i < options.length; i++) {
                    if (!LangUtil.isEmpty(options[i])) {
                        parentOptions.add(options[i]);
                    }
				}
            }
        }
        
        /** 
         * Copy values from another RT 
         * @param toCopy the RT to copy from 
         * @throws IllegalArgumentException if toCopy is null
         */
        public void copy(RT toCopy) {
            LangUtil.throwIaxIfNull(toCopy, "parent");
            parentOptions.clear();
            parentOptions.addAll(toCopy.parentOptions);
            verbose = toCopy.verbose;
        }
        
        /** 
         * Return any parent option accepted by validOptions,
         * optionally removing the parent option.
         * @param validOptions String[] of options to extract
         * @param remove if true, then remove any parent option matched
         * @return String[] containing any validOptions[i] in parentOptions
         *          
         */
        public Values extractOptions(
            Options validOptions, 
            boolean remove,
            StringBuffer errors) {
            Values result = Values.EMPTY;
            if (null == errors) {
                errors = new StringBuffer();
            }
            if (null == validOptions) {
                errors.append("null options");
                return result;                
            }
            if (LangUtil.isEmpty(parentOptions)) {
                return result;
            }
//            boolean haveOption = false;
            String[] parents = (String[]) parentOptions.toArray(new String[0]);
            try {
                result = validOptions.acceptInput(parents);
            } catch (InvalidInputException e) {
                errors.append(e.getFullMessage());
                return result;
            }
            if (remove) {
                Option.Value[] values = result.asArray();
                for (int i = 0; i < values.length; i++) {
                    Option.Value value = values[i];
                    if (null == value) {
                        continue;
                    }
                    final int max = i + value.option.numArguments();
                    if (max > i) {
                        if (max >= parents.length) {
                            errors.append("expecting more args for "
                                + value.option
                                + " at ["
                                + i
                                + "]: "
                                + Arrays.asList(parents));
                            return result;
                        }
                        // XXX verify
                        for (int j = i;  j < max ; j++) {
                            parentOptions.remove(parents[j]);
                        }
                        i = max-1;
                    }
                }
            }
            return result;
        }

        /** 
         * Return any parent option which has one of validOptions as a prefix,
         * optionally absorbing (removing) the parent option.
         * @param validOptions String[] of options to extract
         * @param absorb if true, then remove any parent option matched
         * @return String[] containing any validOptions[i] in parentOptions
         *          (at most once)
         */
        public String[] extractOptions(String[] validOptions, boolean absorb) {
            if (LangUtil.isEmpty(validOptions) || LangUtil.isEmpty(parentOptions)) {
                return new String[0];
            }
            ArrayList result = new ArrayList();
//            boolean haveOption = false;
            for (int i = 0; i < validOptions.length; i++) {
				String option = validOptions[i];
                if (LangUtil.isEmpty(option)) {
                    continue;
                }
                for (ListIterator iter = parentOptions.listIterator(); iter.hasNext();) {
					String parentOption = (String) iter.next();
					if (parentOption.startsWith(option)) {
                        result.add(parentOption);
                        if (absorb) {
                            iter.remove();
                        }
                    }
				}
			}
            return (String[]) result.toArray(new String[0]);
        }
        
        /** Get ListIterator that permits removals */
        ListIterator getListIterator() {
            return parentOptions.listIterator();
        }
        
        /**
		 * Enable verbose logging
		 * @param verbose if true, do verbose logging
         */
		public void setVerbose(boolean verbose) {
            if (this.verbose != verbose) {
                this.verbose = verbose;
            }
		}
    }   // class RT
    
	protected static class Fork {
		public static final Fork NOFORK = new Fork();
		private static final String DELIMITER = ",";
		private final boolean fork;
		private final String version;
		private final String java;
		private final File javaHome;
		private final String bootclasspath;
		private final String errs;

		private Fork() {
			this(null);
		}

		/**
		 * @param spec a String null (no) or of the form
		 *   <code>1.[12345],{javaHome},{java},{bootclasspath}</code>
		 *   where {javaHome} is a readable directory and
		 *   a prefix of {java}.
		 */
		private Fork(String spec) {
			if (null == spec) {
				fork = false;
				version = null;
				java = null;
				javaHome = null;
				bootclasspath = null;
				errs = null;
				return;
			}
			String inputVersion = null;
			File inputJavaHome = null;
			String inputJava = null;
			String inputBootclasspath = null;
			//
			// paths should be in system-specific form
			final String EXPECT = 
				"{version=1.[12345],{java.home},{java.command},{bootclasspath}";
			final String EXAMPLE =
				"1.1,d:/jdk11,d:/jdk11/bin/java,d:/jdk11/lib/classes.zip";
			if (LangUtil.isEmpty(spec)) {
				spec = "";
			}
			StringBuffer inputErrs = new StringBuffer();
			StringTokenizer st = new StringTokenizer(spec, DELIMITER);
			if (4 != st.countTokens()) {
				inputErrs.append(" expecting 4 tokens.");
			} else {
				inputVersion = st.nextToken().trim();
				String inputJavaHomePath = st.nextToken().trim();
				inputJava = st.nextToken().trim();
				inputBootclasspath = st.nextToken().trim();
				if (!inputVersion.startsWith("1.")
					|| (3 != inputVersion.length())
					|| ('1' > inputVersion.charAt(2))
					|| ('6' < inputVersion.charAt(2)) ) {
					inputErrs.append(" expecting version 1.[12345]");
				}
				inputJavaHome = new File(inputJavaHomePath);
				if (!inputJavaHome.canRead()
					|| !inputJavaHome.isDirectory()) {
					inputErrs.append(" expecting java.home dir: "
						+ inputJavaHomePath);
				}
				if (!inputJava.startsWith(inputJavaHomePath)) {
					inputErrs.append(" expecting java in java.home.dir: "
					+ inputJava);
				}
				if (LangUtil.isEmpty(inputBootclasspath)) {
					inputBootclasspath = null;
				}
			}
			String inputErrString = inputErrs.toString();
			fork = (0 == inputErrString.length());
			if (fork) {
				errs = null;
			} else {
				errs = "bad fork specification.  Expecting "
					+ EXPECT
					+ " - for example, "
					+ EXAMPLE
					+ ". Problems: "
					+ inputErrString;
			}
			version = inputVersion;
			java = inputJava;
			javaHome = inputJavaHome;
			bootclasspath = inputBootclasspath;
		}
		public boolean fork() {
			return fork;
		}
		public String getJavaExecutablePath() {
			return java;
		}
		public File getJavaHome() {
			return javaHome;
		}
		public String getJavaBootclasspath() {
			return bootclasspath;
		}
		public String getErrors() {
			return errs;
		}
		public String toSpecString() {
			if (!fork) {
				return null;
			}
			return version
				+ DELIMITER
				+ javaHome
				+ DELIMITER
				+ java
				+ DELIMITER
				+ bootclasspath;
		}
		public String toString() {
			return (fork ? "Fork [true]" : "Fork [false]"); // XXX upgrade
		}
	} // class Fork
}
