/* ******************************************************************* * 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 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 * Wes Isberg 2004 updates * ******************************************************************/ package org.aspectj.testing.harness.bridge; 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 org.aspectj.bridge.IMessage; import org.aspectj.bridge.IMessageHandler; import org.aspectj.bridge.IMessageHolder; import org.aspectj.bridge.ISourceLocation; import org.aspectj.bridge.Message; import org.aspectj.bridge.MessageHandler; import org.aspectj.bridge.MessageUtil; import org.aspectj.testing.run.IRunIterator; import org.aspectj.testing.util.BridgeUtil; import org.aspectj.testing.util.options.Option; import org.aspectj.testing.util.options.Option.InvalidInputException; import org.aspectj.testing.util.options.Options; import org.aspectj.testing.util.options.Values; 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. *

* initialization: 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. *

* subclassing: 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. *

* bean properties: 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). *

* Coordination with writers: 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 { /** 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 keywords; protected final IMessageHolder /* IMessage */messages; protected final ArrayList options; protected final ArrayList paths; // XXXXXunused protected final ArrayList /*ISourceLocation*/ sourceLocations; // XXX remove? protected final ArrayList children; protected final ArrayList 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(); // XXXXXunused 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); } /** @return ((null == s) || (0 == s.trim().length())); */ public static boolean isEmptyTrimmed(String s) { return ((null == s) || (0 == s.length()) || (0 == s.trim().length())); } /** Add keyword if non-empty and not duplicate */ public void addKeyword(String keyword) { if (!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]); } } } // --------------- (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]); } 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]); } } } // --------------------- 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 { String m = "not message: " + o; addMessage(new Message(m, IMessage.WARNING, null, null)); } } } } /** @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++) { IRunSpec 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) { AbstractRunSpec arsChild = (AbstractRunSpec) child; if (!arsChild.adoptParentValues(runtime, handler)) { skipSet.set(i); if (!skipped) { skipped = true; if (skipIfAnyChildSkipped) { // no need to continue checking skipAll = true; 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); // XXXXXunused 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); // XXXXXunused 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 = 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(); // XXXXXunused spec.sourceLocations.addAll(sourceLocations); spec.xmlElementName = xmlElementName; spec.xmlNames = ((AbstractRunSpec.XMLNames) xmlNames.clone()); } 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. XXX this really should be replaced with nested * properties associated logical name with actual name (or placeholders for "unused" and "default"). */ 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); /** 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 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 = 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 }