]> source.dussan.org Git - aspectj.git/commitdiff
formatter chewed on it
authoraclement <aclement>
Thu, 28 Aug 2008 16:27:18 +0000 (16:27 +0000)
committeraclement <aclement>
Thu, 28 Aug 2008 16:27:18 +0000 (16:27 +0000)
util/src/org/aspectj/util/FileUtil.java
util/src/org/aspectj/util/LangUtil.java

index cfea7e533fac50424b53f771ab18315301fb59ed..b0cd73a3b6a2889839c4eb9ae0dc6f7d384e3008 100644 (file)
 
 package org.aspectj.util;
 
-import java.io.*;
-import java.net.*;
-import java.util.*;
-import java.util.zip.*;
-
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.Writer;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipInputStream;
 
 /**
  * 
  */
 public class FileUtil {
-    /** default parent directory File when a file has a null parent */
-    public static final File DEFAULT_PARENT = new File("."); // XXX user.dir?
-
-    /** unmodifiable List of String source file suffixes (including leading ".") */
-    public static final List SOURCE_SUFFIXES
-        = Collections.unmodifiableList(Arrays.asList(new String[] { ".java", ".aj"}));
-    
-    public static final FileFilter ZIP_FILTER = new FileFilter() {
-        public boolean accept(File file) {
-            return isZipFile(file);
-        }
-        public String toString() { 
-            return "ZIP_FILTER"; 
-        }
-    };
-
-    public static final FileFilter SOURCE_FILTER = new FileFilter() {
-        public boolean accept(File file) {
-            return hasSourceSuffix(file);
-        }
-        public String toString() { 
-            return "SOURCE_FILTER"; 
-        }
-    };
-
-    final static int[] INT_RA = new int[0];
-    
-    /** accept all files */
-    public static final FileFilter ALL = new FileFilter() {
-        public boolean accept(File f) { return true; }
-    };
-    public static final FileFilter DIRS_AND_WRITABLE_CLASSES
-        = new FileFilter() {
-            public boolean accept(File file) {
-                return ((null != file)
-                    && (file.isDirectory()
-                        || (file.canWrite()
-                            && file.getName().toLowerCase().endsWith(".class"))));
-            }
-        };
-    private static final boolean PERMIT_CVS;
-    static {
-        String name = FileUtil.class.getName() + ".PERMIT_CVS";
-        PERMIT_CVS = LangUtil.getBoolean(name, false);
-    }
-
-    /** @return true if file exists and is a zip file */
-    public static boolean isZipFile(File file) {
-        try {
+       /** default parent directory File when a file has a null parent */
+       public static final File DEFAULT_PARENT = new File("."); // XXX user.dir?
+
+       /** unmodifiable List of String source file suffixes (including leading ".") */
+       public static final List SOURCE_SUFFIXES = Collections.unmodifiableList(Arrays.asList(new String[] { ".java", ".aj" }));
+
+       public static final FileFilter ZIP_FILTER = new FileFilter() {
+               public boolean accept(File file) {
+                       return isZipFile(file);
+               }
+
+               public String toString() {
+                       return "ZIP_FILTER";
+               }
+       };
+
+       public static final FileFilter SOURCE_FILTER = new FileFilter() {
+               public boolean accept(File file) {
+                       return hasSourceSuffix(file);
+               }
+
+               public String toString() {
+                       return "SOURCE_FILTER";
+               }
+       };
+
+       final static int[] INT_RA = new int[0];
+
+       /** accept all files */
+       public static final FileFilter ALL = new FileFilter() {
+               public boolean accept(File f) {
+                       return true;
+               }
+       };
+       public static final FileFilter DIRS_AND_WRITABLE_CLASSES = new FileFilter() {
+               public boolean accept(File file) {
+                       return ((null != file) && (file.isDirectory() || (file.canWrite() && file.getName().toLowerCase().endsWith(".class"))));
+               }
+       };
+       private static final boolean PERMIT_CVS;
+       static {
+               String name = FileUtil.class.getName() + ".PERMIT_CVS";
+               PERMIT_CVS = LangUtil.getBoolean(name, false);
+       }
+
+       /** @return true if file exists and is a zip file */
+       public static boolean isZipFile(File file) {
+               try {
                        return (null != file) && new ZipFile(file) != null;
                } catch (IOException e) {
                        return false;
                }
-    }
-
-    /** @return true if path ends with .zip or .jar */
-//    public static boolean hasZipSuffix(String path) {
-//        return ((null != path) && (0 != zipSuffixLength(path)));
-//    }
-    
-    /** @return 0 if file has no zip/jar suffix or 4 otherwise  */
-    public static int zipSuffixLength(File file) {
-        return (null == file ? 0 : zipSuffixLength(file.getPath()));
-    }
-    
-    /** @return 0 if no zip/jar suffix or 4 otherwise */
-    public static int zipSuffixLength(String path) {
-        if ((null != path) && (4 < path.length())){
-            String test = path.substring(path.length()-4).toLowerCase();
-            if (".zip".equals(test) || ".jar".equals(test)) {
-                return 4;
-            } 
-        }
-        return 0;
-    }
-
-    /** @return true if file path has a source suffix */
-    public static boolean hasSourceSuffix(File file) {
-        return ((null != file) && hasSourceSuffix(file.getPath()));
-    }
-
-    /** @return true if path ends with .java or .aj */
-    public static boolean hasSourceSuffix(String path) {
-        return ((null != path) && (0 != sourceSuffixLength(path)));
-    }
-    
-    /** @return 0 if file has no source suffix or the length of the suffix otherwise */
-    public static int sourceSuffixLength(File file) {
-        return (null == file ? 0 : sourceSuffixLength(file.getPath()));
-    }
-    
-    /** @return 0 if no source suffix or the length of the suffix otherwise */
-    public static int sourceSuffixLength(String path) {
-        if (LangUtil.isEmpty(path)) {
-            return 0;
-        }
-        
-        for (Iterator iter = SOURCE_SUFFIXES.iterator(); iter.hasNext();) {
+       }
+
+       /** @return true if path ends with .zip or .jar */
+       // public static boolean hasZipSuffix(String path) {
+       // return ((null != path) && (0 != zipSuffixLength(path)));
+       // }
+       /** @return 0 if file has no zip/jar suffix or 4 otherwise */
+       public static int zipSuffixLength(File file) {
+               return (null == file ? 0 : zipSuffixLength(file.getPath()));
+       }
+
+       /** @return 0 if no zip/jar suffix or 4 otherwise */
+       public static int zipSuffixLength(String path) {
+               if ((null != path) && (4 < path.length())) {
+                       String test = path.substring(path.length() - 4).toLowerCase();
+                       if (".zip".equals(test) || ".jar".equals(test)) {
+                               return 4;
+                       }
+               }
+               return 0;
+       }
+
+       /** @return true if file path has a source suffix */
+       public static boolean hasSourceSuffix(File file) {
+               return ((null != file) && hasSourceSuffix(file.getPath()));
+       }
+
+       /** @return true if path ends with .java or .aj */
+       public static boolean hasSourceSuffix(String path) {
+               return ((null != path) && (0 != sourceSuffixLength(path)));
+       }
+
+       /** @return 0 if file has no source suffix or the length of the suffix otherwise */
+       public static int sourceSuffixLength(File file) {
+               return (null == file ? 0 : sourceSuffixLength(file.getPath()));
+       }
+
+       /** @return 0 if no source suffix or the length of the suffix otherwise */
+       public static int sourceSuffixLength(String path) {
+               if (LangUtil.isEmpty(path)) {
+                       return 0;
+               }
+
+               for (Iterator iter = SOURCE_SUFFIXES.iterator(); iter.hasNext();) {
                        String suffix = (String) iter.next();
-            if (path.endsWith(suffix)
-                || path.toLowerCase().endsWith(suffix)) {
-                return suffix.length();
-            }
-               } 
-        return 0;
-    }
-
-    /** @return true if this is a readable directory */
-    public static boolean canReadDir(File dir) {
-        return ((null != dir) && dir.canRead() && dir.isDirectory());
-    }
-    
-    /** @return true if this is a readable file */
-    public static boolean canReadFile(File file) {
-        return ((null != file) && file.canRead() && file.isFile());
-    }
-    
-    /** @return true if dir is a writable directory */
-    public static boolean canWriteDir(File dir) {
-        return ((null != dir) && dir.canWrite() && dir.isDirectory());
-    }
-    
-    /** @return true if this is a writable file */
-    public static boolean canWriteFile(File file) {
-        return ((null != file) && file.canWrite() && file.isFile());
-    }
-    
-    /** @throws IllegalArgumentException unless file is readable and not a directory */
-    public static void throwIaxUnlessCanReadFile(File file, String label) {
-        if (!canReadFile(file)) {
-            throw new IllegalArgumentException(label + " not readable file: " + file);
-        }
-    }
-
-    /** @throws IllegalArgumentException unless dir is a readable directory */
-    public static void throwIaxUnlessCanReadDir(File dir, String label) {
-        if (!canReadDir(dir)) {
-            throw new IllegalArgumentException(label + " not readable dir: " + dir);
-        }
-    }
-    
-    /** @throws IllegalArgumentException unless file is readable and not a directory */
-    public static void throwIaxUnlessCanWriteFile(File file, String label) {
-        if (!canWriteFile(file)) {
-            throw new IllegalArgumentException(label + " not writable file: " + file);
-        }
-    }
-
-    /** @throws IllegalArgumentException unless dir is a readable directory */
-    public static void throwIaxUnlessCanWriteDir(File dir, String label) {
-        if (!canWriteDir(dir)) {
-            throw new IllegalArgumentException(label + " not writable dir: " + dir);
-        }
-    }
-    
-    /** @return array same length as input, with String paths */
-    public static String[] getPaths(File[] files) {
-        if ((null == files) || (0 == files.length)) {
-            return new String[0];
-        }
-        String[] result = new String[files.length];
-        for (int i = 0; i < result.length; i++) {
+                       if (path.endsWith(suffix) || path.toLowerCase().endsWith(suffix)) {
+                               return suffix.length();
+                       }
+               }
+               return 0;
+       }
+
+       /** @return true if this is a readable directory */
+       public static boolean canReadDir(File dir) {
+               return ((null != dir) && dir.canRead() && dir.isDirectory());
+       }
+
+       /** @return true if this is a readable file */
+       public static boolean canReadFile(File file) {
+               return ((null != file) && file.canRead() && file.isFile());
+       }
+
+       /** @return true if dir is a writable directory */
+       public static boolean canWriteDir(File dir) {
+               return ((null != dir) && dir.canWrite() && dir.isDirectory());
+       }
+
+       /** @return true if this is a writable file */
+       public static boolean canWriteFile(File file) {
+               return ((null != file) && file.canWrite() && file.isFile());
+       }
+
+       /** @throws IllegalArgumentException unless file is readable and not a directory */
+       public static void throwIaxUnlessCanReadFile(File file, String label) {
+               if (!canReadFile(file)) {
+                       throw new IllegalArgumentException(label + " not readable file: " + file);
+               }
+       }
+
+       /** @throws IllegalArgumentException unless dir is a readable directory */
+       public static void throwIaxUnlessCanReadDir(File dir, String label) {
+               if (!canReadDir(dir)) {
+                       throw new IllegalArgumentException(label + " not readable dir: " + dir);
+               }
+       }
+
+       /** @throws IllegalArgumentException unless file is readable and not a directory */
+       public static void throwIaxUnlessCanWriteFile(File file, String label) {
+               if (!canWriteFile(file)) {
+                       throw new IllegalArgumentException(label + " not writable file: " + file);
+               }
+       }
+
+       /** @throws IllegalArgumentException unless dir is a readable directory */
+       public static void throwIaxUnlessCanWriteDir(File dir, String label) {
+               if (!canWriteDir(dir)) {
+                       throw new IllegalArgumentException(label + " not writable dir: " + dir);
+               }
+       }
+
+       /** @return array same length as input, with String paths */
+       public static String[] getPaths(File[] files) {
+               if ((null == files) || (0 == files.length)) {
+                       return new String[0];
+               }
+               String[] result = new String[files.length];
+               for (int i = 0; i < result.length; i++) {
                        if (null != files[i]) {
-                result[i] = files[i].getPath();
-            }
-               }
-        return result;
-    }
-
-    /** @return array same length as input, with String paths */
-    public static String[] getPaths(List files) {
-        final int size = (null == files ? 0 : files.size());
-        if (0 == size) {
-            return new String[0];
-        }
-        String[] result = new String[size];
-        for (int i = 0; i < size; i++) {
-            File file = (File) files.get(i);
-            if (null != file) {
-                result[i] = file.getPath();
-            }
-        }
-        return result;
-    }
-
-
-    /**
-     * Extract the name of a class from the path to its file.
-     * If the basedir is null, then the class is assumed to be in
-     * the default package unless the classFile has one of the 
-     * top-level suffixes { com, org, java, javax } as a parent directory.
-     * @param basedir the File of the base directory (prefix of classFile)
-     * @param classFile the File of the class to extract the name for
-     * @throws IllegalArgumentException if classFile is null or does not end with 
-     *          ".class" or a non-null basedir is not a prefix of classFile
-     */
-    public static String fileToClassName(File basedir, File classFile) {
-        LangUtil.throwIaxIfNull(classFile, "classFile");
-        String classFilePath = normalizedPath(classFile);
-        if (!classFilePath.endsWith(".class")) {
-            String m = classFile + " does not end with .class";
-            throw new IllegalArgumentException(m);
-        }
-        classFilePath = classFilePath.substring(0, classFilePath.length()-6);
-        if (null != basedir) {
-            String basePath = normalizedPath(basedir);
-            if (!classFilePath.startsWith(basePath)) {
-                String m = classFile + " does not start with " + basedir;
-                throw new IllegalArgumentException(m);
-            }    
-            classFilePath = classFilePath.substring(basePath.length()+1);
-        } else {
-            final String[] suffixes = new String[] { "com", "org", "java", "javax"};
-            boolean found = false;
-            for (int i = 0; !found && (i < suffixes.length); i++) {
+                               result[i] = files[i].getPath();
+                       }
+               }
+               return result;
+       }
+
+       /** @return array same length as input, with String paths */
+       public static String[] getPaths(List files) {
+               final int size = (null == files ? 0 : files.size());
+               if (0 == size) {
+                       return new String[0];
+               }
+               String[] result = new String[size];
+               for (int i = 0; i < size; i++) {
+                       File file = (File) files.get(i);
+                       if (null != file) {
+                               result[i] = file.getPath();
+                       }
+               }
+               return result;
+       }
+
+       /**
+        * Extract the name of a class from the path to its file. If the basedir is null, then the class is assumed to be in the default
+        * package unless the classFile has one of the top-level suffixes { com, org, java, javax } as a parent directory.
+        * 
+        * @param basedir the File of the base directory (prefix of classFile)
+        * @param classFile the File of the class to extract the name for
+        * @throws IllegalArgumentException if classFile is null or does not end with ".class" or a non-null basedir is not a prefix of
+        *         classFile
+        */
+       public static String fileToClassName(File basedir, File classFile) {
+               LangUtil.throwIaxIfNull(classFile, "classFile");
+               String classFilePath = normalizedPath(classFile);
+               if (!classFilePath.endsWith(".class")) {
+                       String m = classFile + " does not end with .class";
+                       throw new IllegalArgumentException(m);
+               }
+               classFilePath = classFilePath.substring(0, classFilePath.length() - 6);
+               if (null != basedir) {
+                       String basePath = normalizedPath(basedir);
+                       if (!classFilePath.startsWith(basePath)) {
+                               String m = classFile + " does not start with " + basedir;
+                               throw new IllegalArgumentException(m);
+                       }
+                       classFilePath = classFilePath.substring(basePath.length() + 1);
+               } else {
+                       final String[] suffixes = new String[] { "com", "org", "java", "javax" };
+                       boolean found = false;
+                       for (int i = 0; !found && (i < suffixes.length); i++) {
                                int loc = classFilePath.indexOf(suffixes[i] + "/");
-                if ((0 == loc) 
-                    ||  ((-1 != loc) && ('/' == classFilePath.charAt(loc-1)))) {
-                    classFilePath = classFilePath.substring(loc);
-                    found = true;
-                }
+                               if ((0 == loc) || ((-1 != loc) && ('/' == classFilePath.charAt(loc - 1)))) {
+                                       classFilePath = classFilePath.substring(loc);
+                                       found = true;
+                               }
+                       }
+                       if (!found) {
+                               int loc = classFilePath.lastIndexOf("/");
+                               if (-1 != loc) { // treat as default package
+                                       classFilePath = classFilePath.substring(loc + 1);
+                               }
+                       }
+               }
+               return classFilePath.replace('/', '.');
+       }
+
+       /**
+        * Normalize path for comparisons by rendering absolute, clipping basedir prefix, trimming and changing '\\' to '/'
+        * 
+        * @param file the File with the path to normalize
+        * @param basedir the File for the prefix of the file to normalize - ignored if null
+        * @return "" if null or normalized path otherwise
+        * @throws IllegalArgumentException if basedir is not a prefix of file
+        */
+       public static String normalizedPath(File file, File basedir) {
+               String filePath = normalizedPath(file);
+               if (null != basedir) {
+                       String basePath = normalizedPath(basedir);
+                       if (filePath.startsWith(basePath)) {
+                               filePath = filePath.substring(basePath.length());
+                               if (filePath.startsWith("/")) {
+                                       filePath = filePath.substring(1);
+                               }
+                       }
+               }
+               return filePath;
+       }
+
+       /**
+        * Render a set of files to String as a path by getting absolute paths of each and delimiting with infix.
+        * 
+        * @param files the File[] to flatten - may be null or empty
+        * @param infix the String delimiter internally between entries (if null, then use File.pathSeparator). (alias to
+        *        <code>flatten(getAbsolutePaths(files), infix)</code>
+        * @return String with absolute paths to entries in order, delimited with infix
+        */
+       public static String flatten(File[] files, String infix) {
+               if (LangUtil.isEmpty(files)) {
+                       return "";
+               }
+               return flatten(getPaths(files), infix);
+       }
+
+       /**
+        * Flatten File[] to String.
+        * 
+        * @param files the File[] of paths to flatten - null ignored
+        * @param infix the String infix to use - null treated as File.pathSeparator
+        */
+       public static String flatten(String[] paths, String infix) {
+               if (null == infix) {
+                       infix = File.pathSeparator;
+               }
+               StringBuffer result = new StringBuffer();
+               boolean first = true;
+               for (int i = 0; i < paths.length; i++) {
+                       String path = paths[i];
+                       if (null == path) {
+                               continue;
+                       }
+                       if (first) {
+                               first = false;
+                       } else {
+                               result.append(infix);
                        }
-            if (!found) {
-                int loc = classFilePath.lastIndexOf("/");
-                if (-1 != loc) { // treat as default package
-                    classFilePath = classFilePath.substring(loc+1);
-                }
-            }
-        }
-        return classFilePath.replace('/', '.');        
-    }
-    
-    /** 
-     * Normalize path for comparisons by rendering absolute,
-     * clipping basedir prefix,
-     *  trimming and changing '\\' to '/'
-     * @param file the File with the path to normalize
-     * @param basedir the File for the prefix of the file to normalize - ignored if null
-     * @return "" if null or normalized path otherwise
-     * @throws IllegalArgumentException if basedir is not a prefix of file
-     */
-    public static String normalizedPath(File file, File basedir) {
-        String filePath = normalizedPath(file);
-        if (null != basedir) {
-            String basePath = normalizedPath(basedir);
-            if (filePath.startsWith(basePath)) {
-                filePath = filePath.substring(basePath.length());
-                if (filePath.startsWith("/")) {
-                    filePath = filePath.substring(1);
-                }
-            }
-        }
-        return filePath;
-    }
-
-    /**
-     * Render a set of files to String as a path by getting absolute
-     * paths of each and delimiting with infix.
-     * @param files the File[] to flatten - may be null or empty
-     * @param infix the String delimiter internally between entries 
-     *        (if null, then use File.pathSeparator).
-     * (alias to <code>flatten(getAbsolutePaths(files), infix)</code> 
-     * @return String with absolute paths to entries in order, 
-     *         delimited with infix
-     */
-    public static String flatten(File[] files, String infix) {
-        if (LangUtil.isEmpty(files)) {
-            return "";
-        }
-        return flatten(getPaths(files), infix);
-    }
-    
-    /**
-     * Flatten File[] to String.
-     * @param files the File[] of paths to flatten - null ignored
-     * @param infix the String infix to use - null treated as File.pathSeparator
-     */
-    public static String flatten(String[] paths, String infix) {
-        if (null == infix) {
-            infix = File.pathSeparator;
-        }
-        StringBuffer result = new StringBuffer();            
-        boolean first = true;
-        for (int i = 0; i < paths.length; i++) {
+                       result.append(path);
+               }
+               return result.toString();
+       }
+
+       /**
+        * Normalize path for comparisons by rendering absolute trimming and changing '\\' to '/'
+        * 
+        * @return "" if null or normalized path otherwise
+        */
+       public static String normalizedPath(File file) {
+               return (null == file ? "" : weakNormalize(file.getAbsolutePath()));
+       }
+
+       /**
+        * Weakly normalize path for comparisons by trimming and changing '\\' to '/'
+        */
+       public static String weakNormalize(String path) {
+               if (null != path) {
+                       path = path.replace('\\', '/').trim();
+               }
+               return path;
+       }
+
+       /**
+        * Get best File for the first-readable path in input paths, treating entries prefixed "sp:" as system property keys. Safe to
+        * call in static initializers.
+        * 
+        * @param paths the String[] of paths to check.
+        * @return null if not found, or valid File otherwise
+        */
+       public static File getBestFile(String[] paths) {
+               if (null == paths) {
+                       return null;
+               }
+               File result = null;
+               for (int i = 0; (null == result) && (i < paths.length); i++) {
                        String path = paths[i];
-            if (null == path) {
-                continue;
-            }
-            if (first) {
-                first = false;
-            } else {
-                result.append(infix);
-            }            
-            result.append(path);
-               }
-        return result.toString();
-    }
-    
-    /** 
-     * Normalize path for comparisons by rendering absolute 
-     *  trimming and changing '\\' to '/'
-     * @return "" if null or normalized path otherwise
-     */
-    public static String normalizedPath(File file) {
-        return (null == file ? "" : weakNormalize(file.getAbsolutePath()));
-    }
-    
-    /** 
-     * Weakly normalize path for comparisons by
-     *  trimming and changing '\\' to '/'
-     */
-    public static String weakNormalize(String path) {
-        if (null != path) {
-            path = path.replace('\\', '/').trim();
-        }
-        return path;
-    }
-    
-    /**
-     * Get best File for the first-readable path in input paths,
-     * treating entries prefixed "sp:" as system property keys.
-     * Safe to call in static initializers.
-     * @param paths the String[] of paths to check.
-     * @return null if not found, or valid File otherwise
-     */
-    public static File getBestFile(String[] paths) {
-        if (null == paths) {
-            return null;
-        }
-        File result = null;
-        for (int i = 0; (null == result) && (i < paths.length); i++) {
-            String path = paths[i];
-            if (null == path) {
-                continue;
-            }
-            if (path.startsWith("sp:")) {
-                try {
-                    path = System.getProperty(path.substring(3));
-                } catch (Throwable t) {
-                    path = null;
-                }
-                if (null == path) {
-                    continue;
-                }
-            }
-            try {
-                File f = new File(path);
-                if (f.exists() && f.canRead()) {
-                    result = FileUtil.getBestFile(f);
-                }
-            } catch (Throwable t) {
-                // swallow
-            }
-        }
-        return result;
-    }
-
-    /**
-     * Render as best file, canonical or absolute.
-     * @param file the File to get the best File for (not null)
-     * @return File of the best-available path
-     * @throws IllegalArgumentException if file is null
-     */
-    public static File getBestFile(File file) {
-        LangUtil.throwIaxIfNull(file, "file");
-        if (file.exists()) {
-            try {
-                return file.getCanonicalFile();
-            } catch (IOException e) {
-                return file.getAbsoluteFile();
-            }
-        } else {
-            return file;
-        }
-    }
-
-    /**
-     * Render as best path, canonical or absolute.
-     * @param file the File to get the path for (not null)
-     * @return String of the best-available path
-     * @throws IllegalArgumentException if file is null
-     */
-    public static String getBestPath(File file) {
-        LangUtil.throwIaxIfNull(file, "file");
-        if (file.exists()) {
-               try {
-                   return file.getCanonicalPath();
-               } catch (IOException e) {
-                   return file.getAbsolutePath();
-               }
-        } else {
-               return file.getPath();
-        }
-    }
-    
-    /** @return array same length as input, with String absolute paths */
-    public static String[] getAbsolutePaths(File[] files) {
-        if ((null == files) || (0 == files.length)) {
-            return new String[0];
-        }
-        String[] result = new String[files.length];
-        for (int i = 0; i < result.length; i++) {
-            if (null != files[i]) {
-                result[i] = files[i].getAbsolutePath();
-            }
-        }
-        return result;
-    }
-    
-    /** 
-     * Recursively delete the contents of dir, but not the dir itself 
-     * @return the total number of files deleted 
-     */    
-    public static int deleteContents(File dir) {
-        return deleteContents(dir, ALL);
-    }
-    
-    /** 
-     * Recursively delete some contents of dir, but not the dir itself.
-     * This deletes any subdirectory which is empty after its files
-     * are deleted.
-     * @return the total number of files deleted 
-     */    
-    public static int deleteContents(File dir, FileFilter filter) {
-        return deleteContents(dir, filter, true);
-    }
-    
-    /** 
-     * Recursively delete some contents of dir, but not the dir itself.
-     * If deleteEmptyDirs is true, this deletes any subdirectory 
-     * which is empty after its files are deleted.
-     * @param dir the File directory (if a file, the the file is deleted)
-     * @return the total number of files deleted 
-     */    
-    public static int deleteContents(File dir, FileFilter filter, 
-                                    boolean deleteEmptyDirs) {
-        if (null == dir) {
-            throw new IllegalArgumentException("null dir");
-        }
-        if ((!dir.exists()) || (!dir.canWrite())) {
-            return 0;
-        }
-        if (!dir.isDirectory()) {
-            dir.delete();
-            return 1;
-        }
-        String[] fromFiles = dir.list();
-        int result = 0;
-        for (int i = 0; i < fromFiles.length; i++) {
-            String string = fromFiles[i];
-            File file = new File(dir, string);
-            if ((null == filter) || filter.accept(file)) {
-                if (file.isDirectory()) {
-                    result += deleteContents(file, filter, deleteEmptyDirs);
-                    if (deleteEmptyDirs && (0 == file.list().length)) {
-                        file.delete();
-                    }
-                } else {
-                    /*boolean ret = */file.delete();
-                    result++;
-                }
-            }
-        }
-        return result;
-    }
-    
-    /** 
-     * Copy contents of fromDir into toDir
-     * @param fromDir must exist and be readable
-     * @param toDir must exist or be creatable and be writable
-     * @return the total number of files copied 
-     */
-    public static int copyDir(File fromDir, File toDir) throws IOException {
-        return copyDir(fromDir, toDir, null, null);
-    }
-    
-    /** 
-     * Recursively copy files in fromDir (with any fromSuffix) to toDir,
-     * replacing fromSuffix with toSuffix if any.
-     * This silently ignores dirs and files that are not readable
-     * but throw IOException for directories that are not writable.
-     * This does not clean out the original contents of toDir.
-     * (subdirectories are not renamed per directory rules)
-     * @param fromSuffix select files with this suffix - select all if null or empty
-     * @param toSuffix replace fromSuffix with toSuffix in the destination file
-     *         name - ignored if null or empty, 
-     *         appended to name if fromSuffix is null or empty
-     * @return the total number of files copied 
-     */
-    public static int copyDir(File fromDir, File toDir,
-                               final String fromSuffix, String toSuffix) throws IOException {
-        return copyDir(fromDir, toDir, fromSuffix, toSuffix, (FileFilter) null);
-    }
-    
-    /** 
-     * Recursively copy files in fromDir (with any fromSuffix) to toDir,
-     * replacing fromSuffix with toSuffix if any,
-     * and adding the destination file to any collector.
-     * This silently ignores dirs and files that are not readable
-     * but throw IOException for directories that are not writable.
-     * This does not clean out the original contents of toDir.
-     * (subdirectories are not renamed per directory rules)
-     * This calls any delegate FilenameFilter to collect any selected file.
-     * @param fromSuffix select files with this suffix - select all if null or empty
-     * @param toSuffix replace fromSuffix with toSuffix in the destination file
-     *         name - ignored if null or empty, 
-     *         appended to name if fromSuffix is null or empty
-     * @param collector the List sink for destination files - ignored if null
-     * @return the total number of files copied 
-     */
-    public static int copyDir(File fromDir, File toDir, final String fromSuffix, 
-                             final String toSuffix, final List collector) throws IOException {
-        //int before = collector.size();        
-        if (null == collector) {
-            return copyDir(fromDir, toDir, fromSuffix, toSuffix);
-        } else {
-            FileFilter collect = new FileFilter() {
-                       public boolean accept(File pathname) {
-                               return collector.add(pathname);
-                       }
-            };
-            return copyDir(fromDir, toDir, fromSuffix, toSuffix, collect);
-         }
-    }
-    
-    /** 
-     * Recursively copy files in fromDir (with any fromSuffix) to toDir,
-     * replacing fromSuffix with toSuffix if any.
-     * This silently ignores dirs and files that are not readable
-     * but throw IOException for directories that are not writable.
-     * This does not clean out the original contents of toDir.
-     * (subdirectories are not renamed per directory rules)
-     * This calls any delegate FilenameFilter to collect any selected file.
-     * @param fromSuffix select files with this suffix - select all if null or empty
-     * @param toSuffix replace fromSuffix with toSuffix in the destination file
-     *         name - ignored if null or empty, 
-     *         appended to name if fromSuffix is null or empty
-     * @return the total number of files copied 
-     */
-    public static int copyDir(File fromDir, File toDir, final String fromSuffix, 
-                             final String toSuffix, final FileFilter delegate) throws IOException {
-                                
-        if ((null == fromDir) || (!fromDir.canRead())) {
-            return 0;
-        }
-        final boolean haveSuffix = ((null != fromSuffix) && (0 < fromSuffix.length()));
-        final int slen = (!haveSuffix ? 0 : fromSuffix.length());
-        
-        if (!toDir.exists()) {
-            toDir.mkdirs();
-        }
-        final String[] fromFiles;
-        if (!haveSuffix) {
-            fromFiles = fromDir.list();
-        } else {
-            FilenameFilter filter = new FilenameFilter() {
-                public boolean accept(File dir, String name) {
-                    return (new File(dir, name).isDirectory()
-                        || (name.endsWith(fromSuffix)));
-                }
-            };
-            fromFiles = fromDir.list(filter);
-        }
-        int result = 0;
-        final int MAX = (null == fromFiles ? 0 : fromFiles.length);
-        for (int i = 0; i < MAX; i++) {
-            String filename = fromFiles[i];
-            File fromFile = new File(fromDir, filename);
-            if (fromFile.canRead()) {
-                if (fromFile.isDirectory()) {
-                    result += copyDir(fromFile, new File(toDir, filename), fromSuffix, toSuffix, delegate);
-                } else if (fromFile.isFile()) {
-                    if (haveSuffix) {
-                        filename = filename.substring(0, filename.length()-slen);
-                    }
-                    if (null != toSuffix) {
-                        filename = filename + toSuffix;
-                    }
-                    File targetFile = new File(toDir, filename);
-                    if ((null == delegate) || delegate.accept(targetFile)) {
-                        copyFile(fromFile, targetFile);
-                    }
-                    result++;
-                }
-            }
-        }
-        return result;
-    }
-        
-    /** 
-     * Recursively list files in srcDir.
-     * @return ArrayList with String paths of File under srcDir (relative to srcDir)
-     */
-    public static String[] listFiles(File srcDir)  {
-        ArrayList result = new ArrayList();
-        if ((null != srcDir) && srcDir.canRead()) {
-            listFiles(srcDir, null, result);
-        }     
-        return (String[]) result.toArray(new String[0]);
-    }
-    
-    public static final FileFilter aspectjSourceFileFilter = new FileFilter() {
+                       if (null == path) {
+                               continue;
+                       }
+                       if (path.startsWith("sp:")) {
+                               try {
+                                       path = System.getProperty(path.substring(3));
+                               } catch (Throwable t) {
+                                       path = null;
+                               }
+                               if (null == path) {
+                                       continue;
+                               }
+                       }
+                       try {
+                               File f = new File(path);
+                               if (f.exists() && f.canRead()) {
+                                       result = FileUtil.getBestFile(f);
+                               }
+                       } catch (Throwable t) {
+                               // swallow
+                       }
+               }
+               return result;
+       }
+
+       /**
+        * Render as best file, canonical or absolute.
+        * 
+        * @param file the File to get the best File for (not null)
+        * @return File of the best-available path
+        * @throws IllegalArgumentException if file is null
+        */
+       public static File getBestFile(File file) {
+               LangUtil.throwIaxIfNull(file, "file");
+               if (file.exists()) {
+                       try {
+                               return file.getCanonicalFile();
+                       } catch (IOException e) {
+                               return file.getAbsoluteFile();
+                       }
+               } else {
+                       return file;
+               }
+       }
+
+       /**
+        * Render as best path, canonical or absolute.
+        * 
+        * @param file the File to get the path for (not null)
+        * @return String of the best-available path
+        * @throws IllegalArgumentException if file is null
+        */
+       public static String getBestPath(File file) {
+               LangUtil.throwIaxIfNull(file, "file");
+               if (file.exists()) {
+                       try {
+                               return file.getCanonicalPath();
+                       } catch (IOException e) {
+                               return file.getAbsolutePath();
+                       }
+               } else {
+                       return file.getPath();
+               }
+       }
+
+       /** @return array same length as input, with String absolute paths */
+       public static String[] getAbsolutePaths(File[] files) {
+               if ((null == files) || (0 == files.length)) {
+                       return new String[0];
+               }
+               String[] result = new String[files.length];
+               for (int i = 0; i < result.length; i++) {
+                       if (null != files[i]) {
+                               result[i] = files[i].getAbsolutePath();
+                       }
+               }
+               return result;
+       }
+
+       /**
+        * Recursively delete the contents of dir, but not the dir itself
+        * 
+        * @return the total number of files deleted
+        */
+       public static int deleteContents(File dir) {
+               return deleteContents(dir, ALL);
+       }
+
+       /**
+        * Recursively delete some contents of dir, but not the dir itself. This deletes any subdirectory which is empty after its files
+        * are deleted.
+        * 
+        * @return the total number of files deleted
+        */
+       public static int deleteContents(File dir, FileFilter filter) {
+               return deleteContents(dir, filter, true);
+       }
+
+       /**
+        * Recursively delete some contents of dir, but not the dir itself. If deleteEmptyDirs is true, this deletes any subdirectory
+        * which is empty after its files are deleted.
+        * 
+        * @param dir the File directory (if a file, the the file is deleted)
+        * @return the total number of files deleted
+        */
+       public static int deleteContents(File dir, FileFilter filter, boolean deleteEmptyDirs) {
+               if (null == dir) {
+                       throw new IllegalArgumentException("null dir");
+               }
+               if ((!dir.exists()) || (!dir.canWrite())) {
+                       return 0;
+               }
+               if (!dir.isDirectory()) {
+                       dir.delete();
+                       return 1;
+               }
+               String[] fromFiles = dir.list();
+               int result = 0;
+               for (int i = 0; i < fromFiles.length; i++) {
+                       String string = fromFiles[i];
+                       File file = new File(dir, string);
+                       if ((null == filter) || filter.accept(file)) {
+                               if (file.isDirectory()) {
+                                       result += deleteContents(file, filter, deleteEmptyDirs);
+                                       if (deleteEmptyDirs && (0 == file.list().length)) {
+                                               file.delete();
+                                       }
+                               } else {
+                                       /* boolean ret = */file.delete();
+                                       result++;
+                               }
+                       }
+               }
+               return result;
+       }
+
+       /**
+        * Copy contents of fromDir into toDir
+        * 
+        * @param fromDir must exist and be readable
+        * @param toDir must exist or be creatable and be writable
+        * @return the total number of files copied
+        */
+       public static int copyDir(File fromDir, File toDir) throws IOException {
+               return copyDir(fromDir, toDir, null, null);
+       }
+
+       /**
+        * Recursively copy files in fromDir (with any fromSuffix) to toDir, replacing fromSuffix with toSuffix if any. This silently
+        * ignores dirs and files that are not readable but throw IOException for directories that are not writable. This does not clean
+        * out the original contents of toDir. (subdirectories are not renamed per directory rules)
+        * 
+        * @param fromSuffix select files with this suffix - select all if null or empty
+        * @param toSuffix replace fromSuffix with toSuffix in the destination file name - ignored if null or empty, appended to name if
+        *        fromSuffix is null or empty
+        * @return the total number of files copied
+        */
+       public static int copyDir(File fromDir, File toDir, final String fromSuffix, String toSuffix) throws IOException {
+               return copyDir(fromDir, toDir, fromSuffix, toSuffix, (FileFilter) null);
+       }
+
+       /**
+        * Recursively copy files in fromDir (with any fromSuffix) to toDir, replacing fromSuffix with toSuffix if any, and adding the
+        * destination file to any collector. This silently ignores dirs and files that are not readable but throw IOException for
+        * directories that are not writable. This does not clean out the original contents of toDir. (subdirectories are not renamed
+        * per directory rules) This calls any delegate FilenameFilter to collect any selected file.
+        * 
+        * @param fromSuffix select files with this suffix - select all if null or empty
+        * @param toSuffix replace fromSuffix with toSuffix in the destination file name - ignored if null or empty, appended to name if
+        *        fromSuffix is null or empty
+        * @param collector the List sink for destination files - ignored if null
+        * @return the total number of files copied
+        */
+       public static int copyDir(File fromDir, File toDir, final String fromSuffix, final String toSuffix, final List collector)
+                       throws IOException {
+               // int before = collector.size();
+               if (null == collector) {
+                       return copyDir(fromDir, toDir, fromSuffix, toSuffix);
+               } else {
+                       FileFilter collect = new FileFilter() {
+                               public boolean accept(File pathname) {
+                                       return collector.add(pathname);
+                               }
+                       };
+                       return copyDir(fromDir, toDir, fromSuffix, toSuffix, collect);
+               }
+       }
+
+       /**
+        * Recursively copy files in fromDir (with any fromSuffix) to toDir, replacing fromSuffix with toSuffix if any. This silently
+        * ignores dirs and files that are not readable but throw IOException for directories that are not writable. This does not clean
+        * out the original contents of toDir. (subdirectories are not renamed per directory rules) This calls any delegate
+        * FilenameFilter to collect any selected file.
+        * 
+        * @param fromSuffix select files with this suffix - select all if null or empty
+        * @param toSuffix replace fromSuffix with toSuffix in the destination file name - ignored if null or empty, appended to name if
+        *        fromSuffix is null or empty
+        * @return the total number of files copied
+        */
+       public static int copyDir(File fromDir, File toDir, final String fromSuffix, final String toSuffix, final FileFilter delegate)
+                       throws IOException {
+
+               if ((null == fromDir) || (!fromDir.canRead())) {
+                       return 0;
+               }
+               final boolean haveSuffix = ((null != fromSuffix) && (0 < fromSuffix.length()));
+               final int slen = (!haveSuffix ? 0 : fromSuffix.length());
+
+               if (!toDir.exists()) {
+                       toDir.mkdirs();
+               }
+               final String[] fromFiles;
+               if (!haveSuffix) {
+                       fromFiles = fromDir.list();
+               } else {
+                       FilenameFilter filter = new FilenameFilter() {
+                               public boolean accept(File dir, String name) {
+                                       return (new File(dir, name).isDirectory() || (name.endsWith(fromSuffix)));
+                               }
+                       };
+                       fromFiles = fromDir.list(filter);
+               }
+               int result = 0;
+               final int MAX = (null == fromFiles ? 0 : fromFiles.length);
+               for (int i = 0; i < MAX; i++) {
+                       String filename = fromFiles[i];
+                       File fromFile = new File(fromDir, filename);
+                       if (fromFile.canRead()) {
+                               if (fromFile.isDirectory()) {
+                                       result += copyDir(fromFile, new File(toDir, filename), fromSuffix, toSuffix, delegate);
+                               } else if (fromFile.isFile()) {
+                                       if (haveSuffix) {
+                                               filename = filename.substring(0, filename.length() - slen);
+                                       }
+                                       if (null != toSuffix) {
+                                               filename = filename + toSuffix;
+                                       }
+                                       File targetFile = new File(toDir, filename);
+                                       if ((null == delegate) || delegate.accept(targetFile)) {
+                                               copyFile(fromFile, targetFile);
+                                       }
+                                       result++;
+                               }
+                       }
+               }
+               return result;
+       }
+
+       /**
+        * Recursively list files in srcDir.
+        * 
+        * @return ArrayList with String paths of File under srcDir (relative to srcDir)
+        */
+       public static String[] listFiles(File srcDir) {
+               ArrayList result = new ArrayList();
+               if ((null != srcDir) && srcDir.canRead()) {
+                       listFiles(srcDir, null, result);
+               }
+               return (String[]) result.toArray(new String[0]);
+       }
+
+       public static final FileFilter aspectjSourceFileFilter = new FileFilter() {
                public boolean accept(File pathname) {
                        String name = pathname.getName().toLowerCase();
                        return name.endsWith(".java") || name.endsWith(".aj");
                }
-    };
-    
-    
-    /** 
-     * Recursively list files in srcDir.
-     * @return ArrayList with String paths of File under srcDir (relative to srcDir)
-     */
-    public static File[] listFiles(File srcDir, FileFilter fileFilter)  {
-        ArrayList result = new ArrayList();
-        if ((null != srcDir) && srcDir.canRead()) {
-            listFiles(srcDir, result, fileFilter);
-        }     
-        return (File[]) result.toArray(new File[result.size()]);
-    }
-
-    /**
-     * Convert String[] paths to File[] as offset of base directory 
-     * @param basedir the non-null File base directory for File to create with paths
-     * @param paths the String[] of paths to create
-     * @return File[] with same length as paths
-     */
-    public static File[] getBaseDirFiles(
-        File basedir, 
-        String[] paths) {
-        return getBaseDirFiles(basedir, paths, (String[]) null);
-    }
-    
-    /**
-     * Convert String[] paths to File[] as offset of base directory 
-     * @param basedir the non-null File base directory for File to create with paths
-     * @param paths the String[] of paths to create
-     * @param suffixes the String[] of suffixes to limit sources to - ignored if null
-     * @return File[] with same length as paths
-     */
-    public static File[] getBaseDirFiles(
-        File basedir, 
-        String[] paths, 
-        String[] suffixes) {
-        LangUtil.throwIaxIfNull(basedir, "basedir");
-        LangUtil.throwIaxIfNull(paths, "paths");
-        File[] result = null;
-        if (!LangUtil.isEmpty(suffixes)) {
-            ArrayList list = new ArrayList();
-            for (int i = 0; i < paths.length; i++) {
-                String path = paths[i];
-                for (int j = 0; j < suffixes.length; j++) {
-                       if (path.endsWith(suffixes[j])) {
-                        list.add(new File(basedir, paths[i]));
-                        break;
-                       }
+       };
+
+       /**
+        * Recursively list files in srcDir.
+        * 
+        * @return ArrayList with String paths of File under srcDir (relative to srcDir)
+        */
+       public static File[] listFiles(File srcDir, FileFilter fileFilter) {
+               ArrayList result = new ArrayList();
+               if ((null != srcDir) && srcDir.canRead()) {
+                       listFiles(srcDir, result, fileFilter);
+               }
+               return (File[]) result.toArray(new File[result.size()]);
+       }
+
+       /**
+        * Convert String[] paths to File[] as offset of base directory
+        * 
+        * @param basedir the non-null File base directory for File to create with paths
+        * @param paths the String[] of paths to create
+        * @return File[] with same length as paths
+        */
+       public static File[] getBaseDirFiles(File basedir, String[] paths) {
+               return getBaseDirFiles(basedir, paths, (String[]) null);
+       }
+
+       /**
+        * Convert String[] paths to File[] as offset of base directory
+        * 
+        * @param basedir the non-null File base directory for File to create with paths
+        * @param paths the String[] of paths to create
+        * @param suffixes the String[] of suffixes to limit sources to - ignored if null
+        * @return File[] with same length as paths
+        */
+       public static File[] getBaseDirFiles(File basedir, String[] paths, String[] suffixes) {
+               LangUtil.throwIaxIfNull(basedir, "basedir");
+               LangUtil.throwIaxIfNull(paths, "paths");
+               File[] result = null;
+               if (!LangUtil.isEmpty(suffixes)) {
+                       ArrayList list = new ArrayList();
+                       for (int i = 0; i < paths.length; i++) {
+                               String path = paths[i];
+                               for (int j = 0; j < suffixes.length; j++) {
+                                       if (path.endsWith(suffixes[j])) {
+                                               list.add(new File(basedir, paths[i]));
+                                               break;
+                                       }
                                }
-            }
-            result = (File[]) list.toArray(new File[0]);
-        } else {
-            result = new File[paths.length];
-            for (int i = 0; i < result.length; i++) {
-                result[i] = newFile(basedir, paths[i]);
-            }
-        }
-        return result;
-    }
-    
-    /**
-     * Create a new File, resolving paths ".." and "." specially.
-     * @param dir the File for the parent directory of the file
-     * @param path the path in the parent directory (filename only?)
-     * @return File for the new file.
-     */
-    private static File newFile(File dir, String path) {
-        if (".".equals(path)) {
-            return dir;
-        } else if ("..".equals(path)) {
-            File parentDir = dir.getParentFile();
-            if (null != parentDir) {
-                return parentDir;
-            } else {
-                return new File(dir, "..");
-            }
-        } else {
-            return new File(dir, path);
-        }
-    }
-
-    /**
-     * Copy files from source dir into destination directory,
-     * creating any needed directories.  This differs from copyDir in not
-     * being recursive; each input with the source dir creates a full path.
-     * However, if the source is a directory, it is copied as such.
-     * @param srcDir an existing, readable directory containing relativePaths files
-     * @param relativePaths a set of paths relative to srcDir to readable File to copy
-     * @param destDir an existing, writable directory to copy files to
-     * @throws IllegalArgumentException if input invalid, IOException if operations fail
-     */
-    public static File[] copyFiles(File srcDir, String[] relativePaths, File destDir) 
-            throws IllegalArgumentException, IOException {
-        final String[] paths = relativePaths;
-        throwIaxUnlessCanReadDir(srcDir, "srcDir");
-        throwIaxUnlessCanWriteDir(destDir, "destDir");
-        LangUtil.throwIaxIfNull(paths, "relativePaths");
-        File[] result = new File[paths.length];
-        for (int i = 0; i < paths.length; i++) {
-            String path = paths[i];
-            LangUtil.throwIaxIfNull(path, "relativePaths-entry");
-            File src = newFile(srcDir, paths[i]);
-            File dest = newFile(destDir, path);
-            File destParent = dest.getParentFile();
-            if (!destParent.exists()) {
-                destParent.mkdirs();
-            }
-            LangUtil.throwIaxIfFalse(canWriteDir(destParent), "dest-entry-parent");
-            copyFile(src, dest); // both file-dir and dir-dir copies
-            result[i] = dest;
-        }
-        return result;
-    }
-
-    /** 
-     * Copy fromFile to toFile, handling file-file, dir-dir, and file-dir
-     * copies.
-     * @param fromFile the File path of the file or directory to copy - must be
-     * readable
-     * @param toFile the File path of the target file or directory - must be
-     * writable (will be created if it does not exist)
-     */
-    public static void copyFile(File fromFile, File toFile) throws IOException {
-        LangUtil.throwIaxIfNull(fromFile, "fromFile");
-        LangUtil.throwIaxIfNull(toFile, "toFile");
-        LangUtil.throwIaxIfFalse(!toFile.equals(fromFile), "same file");
-        if (toFile.isDirectory()) {   // existing directory 
-            throwIaxUnlessCanWriteDir(toFile, "toFile");
-            if (fromFile.isFile()) {  // file-dir
-                File targFile = new File(toFile, fromFile.getName());
-                copyValidFiles(fromFile, targFile);
-            } else if (fromFile.isDirectory()) { // dir-dir
-                copyDir(fromFile, toFile);
-            } else {
-                LangUtil.throwIaxIfFalse(false, "not dir or file: " + fromFile);
-            }
-        } else if (toFile.isFile()) {     // target file exists
-            if (fromFile.isDirectory()) {
-                LangUtil.throwIaxIfFalse(false, "can't copy to file dir: " + fromFile);
-            }
-            copyValidFiles(fromFile, toFile); // file-file                
-        } else { // target file is a non-existent path -- could be file or dir
-            /*File toFileParent = */ensureParentWritable(toFile);
-            if (fromFile.isFile()) {
-                copyValidFiles(fromFile, toFile);
-            } else if (fromFile.isDirectory()) {
-                toFile.mkdirs();
-                throwIaxUnlessCanWriteDir(toFile, "toFile");
-                copyDir(fromFile, toFile);                
-            } else {
-                LangUtil.throwIaxIfFalse(false, "not dir or file: " + fromFile);
-            }
-        }
-    }
-    
-    /**
-     * Ensure that the parent directory to path can be written.
-     * If the path has a null parent, DEFAULT_PARENT is tested.
-     * If the path parent does not exist, this tries to create it.
-     * @param path the File path whose parent should be writable
-     * @return the File path of the writable parent directory
-     * @throws IllegalArgumentException if parent cannot be written
-     *         or path is null.
-     */
-    public static File ensureParentWritable(File path) {
-        LangUtil.throwIaxIfNull(path, "path");
-        File pathParent = path.getParentFile();
-        if (null == pathParent) {
-            pathParent = DEFAULT_PARENT;     
-        }
-        if (!pathParent.canWrite()) {
-            pathParent.mkdirs();
-        }
-        throwIaxUnlessCanWriteDir(pathParent, "pathParent");
-        return pathParent;
-    }
-    
-    /** 
-     * Copy file to file.
-     * @param fromFile the File to copy (readable, non-null file)
-     * @param toFile the File to copy to (non-null, parent dir exists)
-     * @throws IOException
-     */
-    public static void copyValidFiles(File fromFile, File toFile) throws IOException {
-        FileInputStream in = null;
-        FileOutputStream out = null;
-        try {
-            in = new FileInputStream(fromFile);
-            out = new FileOutputStream(toFile);
-            copyStream(in, out);
-        } finally {
-            if (out != null) {
-                out.close();
-            }
-            if (in != null) {
-                in.close();
-            }
-        }
-    }
-    
-    /** do line-based copying */
-    public static void copyStream(DataInputStream in, PrintStream out) throws IOException {
-        LangUtil.throwIaxIfNull(in, "in");
-        LangUtil.throwIaxIfNull(in, "out");
-        String s;
-        while (null != (s = in.readLine())) {
-            out.println(s);
-        }
-    }
-
-    public static void copyStream(InputStream in, OutputStream out) throws IOException {
-        final int MAX = 4096;
-        byte[] buf = new byte[MAX];
-        for (int bytesRead = in.read(buf, 0, MAX);
-            bytesRead != -1;
-            bytesRead = in.read(buf, 0, MAX)) {
-            out.write(buf, 0, bytesRead);
-        }
-    }
-
-    public static void copyStream(Reader in, Writer out) throws IOException {
-        final int MAX = 4096;
-        char[] buf = new char[MAX];
-        for (int bytesRead = in.read(buf, 0, MAX);
-            bytesRead != -1;
-            bytesRead = in.read(buf, 0, MAX)) {
-            out.write(buf, 0, bytesRead);
-        }
-    }
-        
-    /** 
-     * Make a new child directory of parent
-     * @param parent a File for the parent (writable)
-     * @param child a prefix for the child directory
-     * @return a File dir that exists with parentDir as the parent file or null
-     */
-    public static File makeNewChildDir(File parent, String child) {
-        if (null == parent || ! parent.canWrite() || !parent.isDirectory()) {
-            throw new IllegalArgumentException("bad parent: " + parent);
-        } else if (null == child) {
-            child = "makeNewChildDir";
-        } else if (!isValidFileName(child)) {
-            throw new IllegalArgumentException("bad child: " + child);
-        }
-        File result = new File(parent, child);
-        int safety = 1000;
-        for (String suffix = FileUtil.randomFileString();
-            ((0 < --safety) && result.exists());
-            suffix = FileUtil.randomFileString()) {
-            result = new File(parent, child+suffix);
-        }
-        if (result.exists()) {
-            System.err.println("exhausted files for child dir in " + parent);
-            return null;
-        } 
-        return ((result.mkdirs() && result.exists()) ? result : null);
-    }
-
-    /**
-     * Make a new temporary directory in the same directory
-     * that the system uses for temporary files, or if
-     * that files, in the current directory.
-     * @param name the preferred (simple) name of the directory - may be null.
-     * @return File of an existing new temp dir, or null if unable to create
-     */
-    public static File getTempDir(String name) {
-        if (null == name) {
-            name = "FileUtil_getTempDir";
-        } else if (!isValidFileName(name)) {
-            throw new IllegalArgumentException(" invalid: " + name);
-        }
-        File result = null;
-        File tempFile = null;
-        try {
-            tempFile = File.createTempFile("ignoreMe", ".txt");
-            File tempParent = tempFile.getParentFile();
-            result = makeNewChildDir(tempParent, name);
-        } catch (IOException t) {
-            result = makeNewChildDir(new File("."), name);
-        } finally {
-            if (null != tempFile) {
-                tempFile.delete();
-            }            
-        }
-        return result;
-    }
-        
-    public static URL[] getFileURLs(File[] files) { 
-        if ((null == files) || (0 == files.length)) {
-            return new URL[0];
-        }
-        URL[] result = new URL[files.length]; // XXX dangerous non-copy...
-        for (int i = 0; i < result.length; i++) {
+                       }
+                       result = (File[]) list.toArray(new File[0]);
+               } else {
+                       result = new File[paths.length];
+                       for (int i = 0; i < result.length; i++) {
+                               result[i] = newFile(basedir, paths[i]);
+                       }
+               }
+               return result;
+       }
+
+       /**
+        * Create a new File, resolving paths ".." and "." specially.
+        * 
+        * @param dir the File for the parent directory of the file
+        * @param path the path in the parent directory (filename only?)
+        * @return File for the new file.
+        */
+       private static File newFile(File dir, String path) {
+               if (".".equals(path)) {
+                       return dir;
+               } else if ("..".equals(path)) {
+                       File parentDir = dir.getParentFile();
+                       if (null != parentDir) {
+                               return parentDir;
+                       } else {
+                               return new File(dir, "..");
+                       }
+               } else {
+                       return new File(dir, path);
+               }
+       }
+
+       /**
+        * Copy files from source dir into destination directory, creating any needed directories. This differs from copyDir in not
+        * being recursive; each input with the source dir creates a full path. However, if the source is a directory, it is copied as
+        * such.
+        * 
+        * @param srcDir an existing, readable directory containing relativePaths files
+        * @param relativePaths a set of paths relative to srcDir to readable File to copy
+        * @param destDir an existing, writable directory to copy files to
+        * @throws IllegalArgumentException if input invalid, IOException if operations fail
+        */
+       public static File[] copyFiles(File srcDir, String[] relativePaths, File destDir) throws IllegalArgumentException, IOException {
+               final String[] paths = relativePaths;
+               throwIaxUnlessCanReadDir(srcDir, "srcDir");
+               throwIaxUnlessCanWriteDir(destDir, "destDir");
+               LangUtil.throwIaxIfNull(paths, "relativePaths");
+               File[] result = new File[paths.length];
+               for (int i = 0; i < paths.length; i++) {
+                       String path = paths[i];
+                       LangUtil.throwIaxIfNull(path, "relativePaths-entry");
+                       File src = newFile(srcDir, paths[i]);
+                       File dest = newFile(destDir, path);
+                       File destParent = dest.getParentFile();
+                       if (!destParent.exists()) {
+                               destParent.mkdirs();
+                       }
+                       LangUtil.throwIaxIfFalse(canWriteDir(destParent), "dest-entry-parent");
+                       copyFile(src, dest); // both file-dir and dir-dir copies
+                       result[i] = dest;
+               }
+               return result;
+       }
+
+       /**
+        * Copy fromFile to toFile, handling file-file, dir-dir, and file-dir copies.
+        * 
+        * @param fromFile the File path of the file or directory to copy - must be readable
+        * @param toFile the File path of the target file or directory - must be writable (will be created if it does not exist)
+        */
+       public static void copyFile(File fromFile, File toFile) throws IOException {
+               LangUtil.throwIaxIfNull(fromFile, "fromFile");
+               LangUtil.throwIaxIfNull(toFile, "toFile");
+               LangUtil.throwIaxIfFalse(!toFile.equals(fromFile), "same file");
+               if (toFile.isDirectory()) { // existing directory
+                       throwIaxUnlessCanWriteDir(toFile, "toFile");
+                       if (fromFile.isFile()) { // file-dir
+                               File targFile = new File(toFile, fromFile.getName());
+                               copyValidFiles(fromFile, targFile);
+                       } else if (fromFile.isDirectory()) { // dir-dir
+                               copyDir(fromFile, toFile);
+                       } else {
+                               LangUtil.throwIaxIfFalse(false, "not dir or file: " + fromFile);
+                       }
+               } else if (toFile.isFile()) { // target file exists
+                       if (fromFile.isDirectory()) {
+                               LangUtil.throwIaxIfFalse(false, "can't copy to file dir: " + fromFile);
+                       }
+                       copyValidFiles(fromFile, toFile); // file-file
+               } else { // target file is a non-existent path -- could be file or dir
+                       /* File toFileParent = */ensureParentWritable(toFile);
+                       if (fromFile.isFile()) {
+                               copyValidFiles(fromFile, toFile);
+                       } else if (fromFile.isDirectory()) {
+                               toFile.mkdirs();
+                               throwIaxUnlessCanWriteDir(toFile, "toFile");
+                               copyDir(fromFile, toFile);
+                       } else {
+                               LangUtil.throwIaxIfFalse(false, "not dir or file: " + fromFile);
+                       }
+               }
+       }
+
+       /**
+        * Ensure that the parent directory to path can be written. If the path has a null parent, DEFAULT_PARENT is tested. If the path
+        * parent does not exist, this tries to create it.
+        * 
+        * @param path the File path whose parent should be writable
+        * @return the File path of the writable parent directory
+        * @throws IllegalArgumentException if parent cannot be written or path is null.
+        */
+       public static File ensureParentWritable(File path) {
+               LangUtil.throwIaxIfNull(path, "path");
+               File pathParent = path.getParentFile();
+               if (null == pathParent) {
+                       pathParent = DEFAULT_PARENT;
+               }
+               if (!pathParent.canWrite()) {
+                       pathParent.mkdirs();
+               }
+               throwIaxUnlessCanWriteDir(pathParent, "pathParent");
+               return pathParent;
+       }
+
+       /**
+        * Copy file to file.
+        * 
+        * @param fromFile the File to copy (readable, non-null file)
+        * @param toFile the File to copy to (non-null, parent dir exists)
+        * @throws IOException
+        */
+       public static void copyValidFiles(File fromFile, File toFile) throws IOException {
+               FileInputStream in = null;
+               FileOutputStream out = null;
+               try {
+                       in = new FileInputStream(fromFile);
+                       out = new FileOutputStream(toFile);
+                       copyStream(in, out);
+               } finally {
+                       if (out != null) {
+                               out.close();
+                       }
+                       if (in != null) {
+                               in.close();
+                       }
+               }
+       }
+
+       /** do line-based copying */
+       public static void copyStream(DataInputStream in, PrintStream out) throws IOException {
+               LangUtil.throwIaxIfNull(in, "in");
+               LangUtil.throwIaxIfNull(in, "out");
+               String s;
+               while (null != (s = in.readLine())) {
+                       out.println(s);
+               }
+       }
+
+       public static void copyStream(InputStream in, OutputStream out) throws IOException {
+               final int MAX = 4096;
+               byte[] buf = new byte[MAX];
+               for (int bytesRead = in.read(buf, 0, MAX); bytesRead != -1; bytesRead = in.read(buf, 0, MAX)) {
+                       out.write(buf, 0, bytesRead);
+               }
+       }
+
+       public static void copyStream(Reader in, Writer out) throws IOException {
+               final int MAX = 4096;
+               char[] buf = new char[MAX];
+               for (int bytesRead = in.read(buf, 0, MAX); bytesRead != -1; bytesRead = in.read(buf, 0, MAX)) {
+                       out.write(buf, 0, bytesRead);
+               }
+       }
+
+       /**
+        * Make a new child directory of parent
+        * 
+        * @param parent a File for the parent (writable)
+        * @param child a prefix for the child directory
+        * @return a File dir that exists with parentDir as the parent file or null
+        */
+       public static File makeNewChildDir(File parent, String child) {
+               if (null == parent || !parent.canWrite() || !parent.isDirectory()) {
+                       throw new IllegalArgumentException("bad parent: " + parent);
+               } else if (null == child) {
+                       child = "makeNewChildDir";
+               } else if (!isValidFileName(child)) {
+                       throw new IllegalArgumentException("bad child: " + child);
+               }
+               File result = new File(parent, child);
+               int safety = 1000;
+               for (String suffix = FileUtil.randomFileString(); ((0 < --safety) && result.exists()); suffix = FileUtil.randomFileString()) {
+                       result = new File(parent, child + suffix);
+               }
+               if (result.exists()) {
+                       System.err.println("exhausted files for child dir in " + parent);
+                       return null;
+               }
+               return ((result.mkdirs() && result.exists()) ? result : null);
+       }
+
+       /**
+        * Make a new temporary directory in the same directory that the system uses for temporary files, or if that files, in the
+        * current directory.
+        * 
+        * @param name the preferred (simple) name of the directory - may be null.
+        * @return File of an existing new temp dir, or null if unable to create
+        */
+       public static File getTempDir(String name) {
+               if (null == name) {
+                       name = "FileUtil_getTempDir";
+               } else if (!isValidFileName(name)) {
+                       throw new IllegalArgumentException(" invalid: " + name);
+               }
+               File result = null;
+               File tempFile = null;
+               try {
+                       tempFile = File.createTempFile("ignoreMe", ".txt");
+                       File tempParent = tempFile.getParentFile();
+                       result = makeNewChildDir(tempParent, name);
+               } catch (IOException t) {
+                       result = makeNewChildDir(new File("."), name);
+               } finally {
+                       if (null != tempFile) {
+                               tempFile.delete();
+                       }
+               }
+               return result;
+       }
+
+       public static URL[] getFileURLs(File[] files) {
+               if ((null == files) || (0 == files.length)) {
+                       return new URL[0];
+               }
+               URL[] result = new URL[files.length]; // XXX dangerous non-copy...
+               for (int i = 0; i < result.length; i++) {
                        result[i] = getFileURL(files[i]);
                }
-        return result;
-    }
-       
-    /**
-     * Get URL for a File. 
-     * This appends "/" for directories.
-     * prints errors to System.err 
-     * @param file the File to convert to URL (not null)
-     */
-    public static URL getFileURL(File file) { 
+               return result;
+       }
+
+       /**
+        * Get URL for a File. This appends "/" for directories. prints errors to System.err
+        * 
+        * @param file the File to convert to URL (not null)
+        */
+       public static URL getFileURL(File file) {
                LangUtil.throwIaxIfNull(file, "file");
-        URL result = null;
-        try {
-            result = file.toURL();//TODO AV - was toURI.toURL that does not works on Java 1.3 
-            if (null != result) {
-                return result;
-            }
-            String url =  "file:" + file.getAbsolutePath().replace('\\', '/');
-            result = new URL(url + (file.isDirectory() ? "/" : ""));
-        } catch (MalformedURLException e) {
-            String m = "Util.makeURL(\"" + file.getPath() + "\" MUE " + e.getMessage();
-            System.err.println(m);
-        }
+               URL result = null;
+               try {
+                       result = file.toURL();// TODO AV - was toURI.toURL that does not works on Java 1.3
+                       if (null != result) {
+                               return result;
+                       }
+                       String url = "file:" + file.getAbsolutePath().replace('\\', '/');
+                       result = new URL(url + (file.isDirectory() ? "/" : ""));
+               } catch (MalformedURLException e) {
+                       String m = "Util.makeURL(\"" + file.getPath() + "\" MUE " + e.getMessage();
+                       System.err.println(m);
+               }
                return result;
        }
-       
-       
-    
-    /**
-     * Write contents to file, returning null on success or error message otherwise.
-     * This tries to make any necessary parent directories first.
-     * @param file the File to write (not null)
-     * @param contents the String to write (use "" if null)
-     * @return String null on no error, error otherwise 
-     */
-    public static String writeAsString(File file, String contents) {
-        LangUtil.throwIaxIfNull(file, "file");
-        if (null == contents) {
-            contents = "";
-        }
-        Writer out = null;
-        try {
-            File parentDir = file.getParentFile();
-            if (!parentDir.exists() && !parentDir.mkdirs()) {
-                return "unable to make parent dir for " + file;
-            }
-            Reader in = new StringReader(contents);
-            out = new FileWriter(file);
-            FileUtil.copyStream(in, out);
-            return null;
-        } catch (IOException e) {
-            return LangUtil.unqualifiedClassName(e) + " writing " + file 
-                + ": " + e.getMessage();
-        } finally {
-            if (null != out) {
-                try { out.close(); }
-                catch (IOException e) {} // ignored
-            }
-        }        
-    }
 
+       /**
+        * Write contents to file, returning null on success or error message otherwise. This tries to make any necessary parent
+        * directories first.
+        * 
+        * @param file the File to write (not null)
+        * @param contents the String to write (use "" if null)
+        * @return String null on no error, error otherwise
+        */
+       public static String writeAsString(File file, String contents) {
+               LangUtil.throwIaxIfNull(file, "file");
+               if (null == contents) {
+                       contents = "";
+               }
+               Writer out = null;
+               try {
+                       File parentDir = file.getParentFile();
+                       if (!parentDir.exists() && !parentDir.mkdirs()) {
+                               return "unable to make parent dir for " + file;
+                       }
+                       Reader in = new StringReader(contents);
+                       out = new FileWriter(file);
+                       FileUtil.copyStream(in, out);
+                       return null;
+               } catch (IOException e) {
+                       return LangUtil.unqualifiedClassName(e) + " writing " + file + ": " + e.getMessage();
+               } finally {
+                       if (null != out) {
+                               try {
+                                       out.close();
+                               } catch (IOException e) {
+                               } // ignored
+                       }
+               }
+       }
 
        /**
         * Reads a boolean array with our encoding
@@ -994,54 +992,53 @@ public class FileUtil {
        public static boolean[] readBooleanArray(DataInputStream s) throws IOException {
                int len = s.readInt();
                boolean[] ret = new boolean[len];
-               for (int i=0; i < len; i++) ret[i] = s.readBoolean();
+               for (int i = 0; i < len; i++)
+                       ret[i] = s.readBoolean();
                return ret;
        }
 
-
        /**
         * Writes a boolean array with our encoding
         */
        public static void writeBooleanArray(boolean[] a, DataOutputStream s) throws IOException {
                int len = a.length;
                s.writeInt(len);
-               for (int i=0; i < len; i++) s.writeBoolean(a[i]);
+               for (int i = 0; i < len; i++)
+                       s.writeBoolean(a[i]);
        }
 
-
        /**
         * Reads an int array with our encoding
         */
        public static int[] readIntArray(DataInputStream s) throws IOException {
                int len = s.readInt();
                int[] ret = new int[len];
-               for (int i=0; i < len; i++) ret[i] = s.readInt();
+               for (int i = 0; i < len; i++)
+                       ret[i] = s.readInt();
                return ret;
        }
 
-
        /**
         * Writes an int array with our encoding
         */
        public static void writeIntArray(int[] a, DataOutputStream s) throws IOException {
                int len = a.length;
                s.writeInt(len);
-               for (int i=0; i < len; i++) s.writeInt(a[i]);
+               for (int i = 0; i < len; i++)
+                       s.writeInt(a[i]);
        }
 
-
-
        /**
         * Reads an int array with our encoding
         */
        public static String[] readStringArray(DataInputStream s) throws IOException {
                int len = s.readInt();
                String[] ret = new String[len];
-               for (int i=0; i < len; i++) ret[i] = s.readUTF();
+               for (int i = 0; i < len; i++)
+                       ret[i] = s.readUTF();
                return ret;
        }
 
-
        /**
         * Writes an int array with our encoding
         */
@@ -1052,10 +1049,10 @@ public class FileUtil {
                }
                int len = a.length;
                s.writeInt(len);
-               for (int i=0; i < len; i++) s.writeUTF(a[i]);
+               for (int i = 0; i < len; i++)
+                       s.writeUTF(a[i]);
        }
 
-
        /**
         * Returns the contents of this file as a String
         */
@@ -1064,13 +1061,14 @@ public class FileUtil {
                StringBuffer b = new StringBuffer();
                while (true) {
                        int ch = r.read();
-                       if (ch == -1) break;
-                       b.append((char)ch);
+                       if (ch == -1)
+                               break;
+                       b.append((char) ch);
                }
                r.close();
                return b.toString();
        }
-       
+
        /**
         * Returns the contents of this stream as a String
         */
@@ -1079,14 +1077,14 @@ public class FileUtil {
                StringBuffer b = new StringBuffer();
                while (true) {
                        int ch = r.read();
-                       if (ch == -1) break;
-                       b.append((char)ch);
+                       if (ch == -1)
+                               break;
+                       b.append((char) ch);
                }
                in.close();
                r.close();
                return b.toString();
        }
-       
 
        /**
         * Returns the contents of this file as a byte[]
@@ -1098,7 +1096,6 @@ public class FileUtil {
                return ret;
        }
 
-
        /**
         * Reads this input stream and returns contents as a byte[]
         */
@@ -1106,10 +1103,11 @@ public class FileUtil {
                int size = 1024;
                byte[] ba = new byte[size];
                int readSoFar = 0;
-               
+
                while (true) {
-                       int nRead = inStream.read(ba, readSoFar, size-readSoFar);
-                       if (nRead == -1) break;
+                       int nRead = inStream.read(ba, readSoFar, size - readSoFar);
+                       if (nRead == -1)
+                               break;
                        readSoFar += nRead;
                        if (readSoFar == size) {
                                int newSize = size * 2;
@@ -1119,54 +1117,55 @@ public class FileUtil {
                                size = newSize;
                        }
                }
-               
+
                byte[] newBa = new byte[readSoFar];
                System.arraycopy(ba, 0, newBa, 0, readSoFar);
                return newBa;
        }
 
-    final static String FILECHARS = "abcdefghijklmnopqrstuvxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
-    /** @return semi-random String of length 6 usable as filename suffix */
-    static String randomFileString() {
-        final double FILECHARS_length = FILECHARS.length();
-        final int LEN = 6;
-        final char[] result = new char[LEN];
-        int index = (int) (Math.random() * 6d);
-        for (int i = 0; i < LEN; i++) {
-            if (index >= LEN) {
-                index = 0;
-            }
-            result[index++] = FILECHARS.charAt((int) (Math.random() * FILECHARS_length));              
+       final static String FILECHARS = "abcdefghijklmnopqrstuvxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+
+       /** @return semi-random String of length 6 usable as filename suffix */
+       static String randomFileString() {
+               final double FILECHARS_length = FILECHARS.length();
+               final int LEN = 6;
+               final char[] result = new char[LEN];
+               int index = (int) (Math.random() * 6d);
+               for (int i = 0; i < LEN; i++) {
+                       if (index >= LEN) {
+                               index = 0;
+                       }
+                       result[index++] = FILECHARS.charAt((int) (Math.random() * FILECHARS_length));
                }
-        return new String(result);     
-    }
+               return new String(result);
+       }
 
        public static InputStream getStreamFromZip(String zipFile, String name) {
                try {
                        ZipFile zf = new ZipFile(zipFile);
                        try {
-                       ZipEntry entry = zf.getEntry(name);
-                       return zf.getInputStream(entry);
+                               ZipEntry entry = zf.getEntry(name);
+                               return zf.getInputStream(entry);
                        } finally {
-                               //??? is it safe not to close this zf.close();
+                               // ??? is it safe not to close this zf.close();
                        }
                } catch (IOException ioe) {
                        return null;
                }
        }
-       
 
        public static void extractJar(String zipFile, String outDir) throws IOException {
                ZipInputStream zs = new ZipInputStream(new FileInputStream(zipFile));
                ZipEntry entry;
-               while ( (entry = zs.getNextEntry()) != null) {
-                       if (entry.isDirectory()) continue;
+               while ((entry = zs.getNextEntry()) != null) {
+                       if (entry.isDirectory())
+                               continue;
                        byte[] in = readAsByteArray(zs);
-                       
+
                        File outFile = new File(outDir + "/" + entry.getName());
-                       //if (!outFile.getParentFile().exists()) 
-                       //System.err.println("parent: " + outFile.getParentFile());
-                       //System.err.println("parent: " + outFile.getParentFile());
+                       // if (!outFile.getParentFile().exists())
+                       // System.err.println("parent: " + outFile.getParentFile());
+                       // System.err.println("parent: " + outFile.getParentFile());
                        outFile.getParentFile().mkdirs();
                        FileOutputStream os = new FileOutputStream(outFile);
                        os.write(in);
@@ -1177,157 +1176,157 @@ public class FileUtil {
        }
 
        /**
-        * Do line-based search  for literal text in source files, 
-     * returning file:line where found.
+        * Do line-based search for literal text in source files, returning file:line where found.
+        * 
         * @param sought the String text to seek in the file
         * @param sources the List of String paths to the source files
-     * @param listAll if false, only list first match in file
-     * @param errorSink the PrintStream to print any errors to (one per line)
-     *         (use null to silently ignore errors)
-        * @return List of String of the form file:line for each found entry
-     *          (never null, might be empty)
+        * @param listAll if false, only list first match in file
+        * @param errorSink the PrintStream to print any errors to (one per line) (use null to silently ignore errors)
+        * @return List of String of the form file:line for each found entry (never null, might be empty)
         */
        // OPTIMIZE only used by tests? move it out
-       public static List lineSeek(String sought, List sources, boolean listAll,
-        PrintStream errorSink) {
-        if (LangUtil.isEmpty(sought) || LangUtil.isEmpty(sources)) {
-            return Collections.EMPTY_LIST;
-        }
-        ArrayList result = new ArrayList();
-        for (Iterator iter = sources.iterator(); iter.hasNext();) {
+       public static List lineSeek(String sought, List sources, boolean listAll, PrintStream errorSink) {
+               if (LangUtil.isEmpty(sought) || LangUtil.isEmpty(sources)) {
+                       return Collections.EMPTY_LIST;
+               }
+               ArrayList result = new ArrayList();
+               for (Iterator iter = sources.iterator(); iter.hasNext();) {
                        String path = (String) iter.next();
-            String error = lineSeek(sought, path, listAll, result);
-            if ((null != error) && (null != errorSink)) {
-                errorSink.println(error);
-            }
-               }
-        return result;
-       }
-    
-    /**
-     * Do line-based search  for literal text in source file, 
-     * returning line where found as a String 
-     * in the form {sourcePath}:line:column submitted to the
-     * collecting parameter sink.
-     * Any error is rendered to String and returned as the result.
-     * 
-     * @param sought the String text to seek in the file
-     * @param sources the List of String paths to the source files
-     * @param listAll if false, only list first match in file
-     * @param List sink the List for String entries of the form {sourcePath}:line:column
-     * @return String error if any, or add String entries to sink
-     */
-    public static String lineSeek(String sought, String sourcePath, boolean listAll,
-        ArrayList sink) {
-        if (LangUtil.isEmpty(sought) || LangUtil.isEmpty(sourcePath)) {
-            return "nothing sought";
-        }
-        if (LangUtil.isEmpty(sourcePath)) {
-            return "no sourcePath";
-        }
-        final File file = new File(sourcePath);
-        if (!file.canRead() || !file.isFile()) {
-            return "sourcePath not a readable file";
-        }
-        int lineNum = 0;
-        FileReader fin = null;
-        try {
-            fin = new FileReader(file);
-            BufferedReader reader = new BufferedReader(fin);
-            String line;
-            while (null != (line = reader.readLine())) {
-                lineNum++;
-                int loc = line.indexOf(sought);
-                if (-1 != loc) {
-                    sink.add(sourcePath + ":" + lineNum + ":" + loc);
-                    if (!listAll) {
-                        break;
-                    }
-                }
-            }
-        } catch (IOException e) {
-            return LangUtil.unqualifiedClassName(e) + " reading " + sourcePath 
-                + ":" + lineNum;
-        } finally {
-            try { if (null != fin) fin.close(); }
-            catch (IOException e) {} // ignore
-        }
-        return null;
-    }
+                       String error = lineSeek(sought, path, listAll, result);
+                       if ((null != error) && (null != errorSink)) {
+                               errorSink.println(error);
+                       }
+               }
+               return result;
+       }
+
+       /**
+        * Do line-based search for literal text in source file, returning line where found as a String in the form
+        * {sourcePath}:line:column submitted to the collecting parameter sink. Any error is rendered to String and returned as the
+        * result.
+        * 
+        * @param sought the String text to seek in the file
+        * @param sources the List of String paths to the source files
+        * @param listAll if false, only list first match in file
+        * @param List sink the List for String entries of the form {sourcePath}:line:column
+        * @return String error if any, or add String entries to sink
+        */
+       public static String lineSeek(String sought, String sourcePath, boolean listAll, ArrayList sink) {
+               if (LangUtil.isEmpty(sought) || LangUtil.isEmpty(sourcePath)) {
+                       return "nothing sought";
+               }
+               if (LangUtil.isEmpty(sourcePath)) {
+                       return "no sourcePath";
+               }
+               final File file = new File(sourcePath);
+               if (!file.canRead() || !file.isFile()) {
+                       return "sourcePath not a readable file";
+               }
+               int lineNum = 0;
+               FileReader fin = null;
+               try {
+                       fin = new FileReader(file);
+                       BufferedReader reader = new BufferedReader(fin);
+                       String line;
+                       while (null != (line = reader.readLine())) {
+                               lineNum++;
+                               int loc = line.indexOf(sought);
+                               if (-1 != loc) {
+                                       sink.add(sourcePath + ":" + lineNum + ":" + loc);
+                                       if (!listAll) {
+                                               break;
+                                       }
+                               }
+                       }
+               } catch (IOException e) {
+                       return LangUtil.unqualifiedClassName(e) + " reading " + sourcePath + ":" + lineNum;
+               } finally {
+                       try {
+                               if (null != fin)
+                                       fin.close();
+                       } catch (IOException e) {
+                       } // ignore
+               }
+               return null;
+       }
 
        public static BufferedOutputStream makeOutputStream(File file) throws FileNotFoundException {
                File parent = file.getParentFile();
-               if (parent != null) parent.mkdirs();
+               if (parent != null)
+                       parent.mkdirs();
                return new BufferedOutputStream(new FileOutputStream(file));
        }
 
-    /**
-     * Sleep until after the last last-modified stamp from the files.
-     * @param files the File[] of files to inspect for last modified times
-     *        (this ignores null or empty files array
-     *         and null or non-existing components of files array)
-     * @return true if succeeded without 100 interrupts
-     */
-    public static boolean sleepPastFinalModifiedTime(File[] files) {
-        if ((null == files) || (0 == files.length)) {
-            return true;
-        }
-        long delayUntil = System.currentTimeMillis();
-        for (int i = 0; i < files.length; i++) {
-            File file = files[i];
-            if ((null == file) || !file.exists()) {
-                continue;
-            }
-            long nextModTime = file.lastModified();
-            if (nextModTime > delayUntil) {
-                delayUntil = nextModTime;
-            }
-        }
-        return LangUtil.sleepUntil(++delayUntil);
-    }
-
-    private static void listFiles(final File baseDir, ArrayList result, FileFilter filter)  {
-        File[] files = baseDir.listFiles();
-        // hack https://bugs.eclipse.org/bugs/show_bug.cgi?id=48650
-        final boolean skipCVS = (! PERMIT_CVS && (filter == aspectjSourceFileFilter));
-        for (int i = 0; i < files.length; i++) {
-            File f = files[i];
-            if (f.isDirectory()) {
-                if (skipCVS) {
-                    String name = f.getName().toLowerCase();
-                    if ("cvs".equals(name) || "sccs".equals(name)) {
-                        continue;
-                    }
-                }
-                listFiles(f, result, filter);
-            } else {
-                if (filter.accept(f)) result.add(f);
-            }
-        }
-    }
-
-    /** @return true if input is not null and contains no path separator */
-    private static boolean isValidFileName(String input) {
-        return ((null != input) && (-1 == input.indexOf(File.pathSeparator)));
-    }
-
-    private static void listFiles(final File baseDir, String dir, ArrayList result)  {
-        final String dirPrefix = (null == dir ? "" : dir + "/");
-        final File dirFile = (null == dir ? baseDir : new File(baseDir.getPath() + "/" + dir));
-        final String[] files = dirFile.list();
-        for (int i = 0; i < files.length; i++) {
-            File f = new File(dirFile, files[i]);
-            String path = dirPrefix + files[i];
-            if (f.isDirectory()) {
-                listFiles(baseDir, path, result);
-            } else {
-                result.add(path);
-            }
-        }
-    }
-
-    private FileUtil() { throw new Error("utility class"); }
-       
+       /**
+        * Sleep until after the last last-modified stamp from the files.
+        * 
+        * @param files the File[] of files to inspect for last modified times (this ignores null or empty files array and null or
+        *        non-existing components of files array)
+        * @return true if succeeded without 100 interrupts
+        */
+       public static boolean sleepPastFinalModifiedTime(File[] files) {
+               if ((null == files) || (0 == files.length)) {
+                       return true;
+               }
+               long delayUntil = System.currentTimeMillis();
+               for (int i = 0; i < files.length; i++) {
+                       File file = files[i];
+                       if ((null == file) || !file.exists()) {
+                               continue;
+                       }
+                       long nextModTime = file.lastModified();
+                       if (nextModTime > delayUntil) {
+                               delayUntil = nextModTime;
+                       }
+               }
+               return LangUtil.sleepUntil(++delayUntil);
+       }
+
+       private static void listFiles(final File baseDir, ArrayList result, FileFilter filter) {
+               File[] files = baseDir.listFiles();
+               // hack https://bugs.eclipse.org/bugs/show_bug.cgi?id=48650
+               final boolean skipCVS = (!PERMIT_CVS && (filter == aspectjSourceFileFilter));
+               for (int i = 0; i < files.length; i++) {
+                       File f = files[i];
+                       if (f.isDirectory()) {
+                               if (skipCVS) {
+                                       String name = f.getName().toLowerCase();
+                                       if ("cvs".equals(name) || "sccs".equals(name)) {
+                                               continue;
+                                       }
+                               }
+                               listFiles(f, result, filter);
+                       } else {
+                               if (filter.accept(f))
+                                       result.add(f);
+                       }
+               }
+       }
+
+       /** @return true if input is not null and contains no path separator */
+       private static boolean isValidFileName(String input) {
+               return ((null != input) && (-1 == input.indexOf(File.pathSeparator)));
+       }
+
+       private static void listFiles(final File baseDir, String dir, ArrayList result) {
+               final String dirPrefix = (null == dir ? "" : dir + "/");
+               final File dirFile = (null == dir ? baseDir : new File(baseDir.getPath() + "/" + dir));
+               final String[] files = dirFile.list();
+               for (int i = 0; i < files.length; i++) {
+                       File f = new File(dirFile, files[i]);
+                       String path = dirPrefix + files[i];
+                       if (f.isDirectory()) {
+                               listFiles(baseDir, path, result);
+                       } else {
+                               result.add(path);
+                       }
+               }
+       }
+
+       private FileUtil() {
+               throw new Error("utility class");
+       }
+
        public static List makeClasspath(URL[] urls) {
                List ret = new LinkedList();
                if (urls != null) {
@@ -1338,175 +1337,162 @@ public class FileUtil {
                return ret;
        }
 
-    /**
-     * A pipe when run reads from an input stream to an output stream,
-     * optionally sleeping between reads.
-     * @see #copyStream(InputStream, OutputStream)
-     */
-    public static class Pipe implements Runnable {
-        private final InputStream in;
-        private final OutputStream out;        
-        private final long sleep;
-        private ByteArrayOutputStream snoop;
-        private long totalWritten;
-        private Throwable thrown;
-        private boolean halt;
-        /** 
-         * Seem to be unable to detect erroroneous closing of System.out...
-         */
-        private final boolean closeInput;
-        private final boolean closeOutput;
-
-        /** 
-         * If true, then continue processing stream until
-         *  no characters are returned when halting.
-         */
-        private boolean finishStream;
-        
-        private boolean done; // true after completing() completes
-
-        /** 
-         * alias for <code>Pipe(in, out, 100l, false, false)</code>
-         * @param in the InputStream source to read
-         * @param out the OutputStream sink to write
-         */
-        Pipe(
-            InputStream in, 
-            OutputStream out) {
-            this(in, out, 100l, false, false);
-        }
-        
-        /** 
-         * @param in the InputStream source to read
-         * @param out the OutputStream sink to write
-         * @param tryClosingStreams if true, then try closing both streams when done
-         * @param sleep milliseconds to delay between reads (pinned to 0..1 minute)
-         */
-        Pipe(
-            InputStream in, 
-            OutputStream out, 
-            long sleep, 
-            boolean closeInput, 
-            boolean closeOutput) {
-            LangUtil.throwIaxIfNull(in, "in");
-            LangUtil.throwIaxIfNull(out, "out");
-            this.in = in;
-            this.out = out;
-            this.closeInput = closeInput;
-            this.closeOutput = closeOutput;
-            this.sleep = Math.min(0l, Math.max(60l*1000l, sleep));
-        }
-
-        public void setSnoop(ByteArrayOutputStream snoop) {
-            this.snoop = snoop;
-        }
-
-        /**
-         * Run the pipe.
-         * This halts on the first Throwable thrown or when a read returns
-         * -1 (for end-of-file) or on demand.
-         */
-        public void run() {
-            totalWritten = 0;
-            if (halt) {
-                return;
-            }
-            try {
-                final int MAX = 4096;
-                byte[] buf = new byte[MAX];
-                // TODO this blocks, hanging the harness
-                int count = in.read(buf, 0, MAX);
-                ByteArrayOutputStream mySnoop;
-                while ((halt && finishStream && (0 < count))
-                    || (!halt && (-1 != count))) {
-                    out.write(buf, 0, count);
-                    mySnoop = snoop;
-                    if (null != mySnoop) {
-                        mySnoop.write(buf, 0, count);
-                    }
-                    totalWritten += count;
-                    if (halt && !finishStream) { 
-                        break; 
-                    }
-                    if (!halt && (0 < sleep)) {
-                        Thread.sleep(sleep);
-                    }
-                    if (halt && !finishStream) { 
-                        break; 
-                    }
-                    count = in.read(buf, 0, MAX);
-                }
-            } catch (Throwable e) {
-                thrown = e;
-            } finally {
-                halt = true;
-                if (closeInput) {
-                    try {
-                        in.close();
-                    } catch (IOException e) {
-                        // ignore
-                    }
-                }
-                if (closeOutput) {
-                    try {
-                        out.close();
-                    } catch (IOException e) {
-                        // ignore
-                    }
-                }
-                done = true;
-                completing(totalWritten, thrown);
-            }
-            
-        }    
-
-        /**
-         * Tell the pipe to halt the next time it gains control.
-         * @param wait if true, this waits synchronously until pipe is done
-         * @param finishStream if true, then continue until 
-         *        a read from the input stream returns no bytes, then halt.
-         * @return true if <code>run()</code> will return the next time it gains control
-         */
-        public boolean halt(boolean wait, boolean finishStream) {
-            if (!halt) {
-                halt = true;
-            }
-            if (wait) {
-                while (!done) {
-                    synchronized (this) {
-                        notifyAll();
-                    }
-                    if (!done) {
-                        try {
-                            Thread.sleep(5l);
-                        } catch (InterruptedException e) {
-                            break;
-                        }
-                    }
-                }
-            }
-            return halt;
-        }
-
-        /** @return the total number of bytes written */
-        public long totalWritten() {
-            return totalWritten;
-        }
-
-        /** @return any exception thrown when reading/writing */
-        public Throwable getThrown() {
-            return thrown;
-        }
-        
-        /**
-         * This is called when the pipe is completing.
-         * This implementation does nothing.
-         * Subclasses implement this to get notice.
-         * Note that halt(true, true) might or might not have completed
-         * before this method is called.
-         */
-        protected void completing(long totalWritten, Throwable thrown) {
-        }
-    }
+       /**
+        * A pipe when run reads from an input stream to an output stream, optionally sleeping between reads.
+        * 
+        * @see #copyStream(InputStream, OutputStream)
+        */
+       public static class Pipe implements Runnable {
+               private final InputStream in;
+               private final OutputStream out;
+               private final long sleep;
+               private ByteArrayOutputStream snoop;
+               private long totalWritten;
+               private Throwable thrown;
+               private boolean halt;
+               /**
+                * Seem to be unable to detect erroroneous closing of System.out...
+                */
+               private final boolean closeInput;
+               private final boolean closeOutput;
+
+               /**
+                * If true, then continue processing stream until no characters are returned when halting.
+                */
+               private boolean finishStream;
+
+               private boolean done; // true after completing() completes
+
+               /**
+                * alias for <code>Pipe(in, out, 100l, false, false)</code>
+                * 
+                * @param in the InputStream source to read
+                * @param out the OutputStream sink to write
+                */
+               Pipe(InputStream in, OutputStream out) {
+                       this(in, out, 100l, false, false);
+               }
+
+               /**
+                * @param in the InputStream source to read
+                * @param out the OutputStream sink to write
+                * @param tryClosingStreams if true, then try closing both streams when done
+                * @param sleep milliseconds to delay between reads (pinned to 0..1 minute)
+                */
+               Pipe(InputStream in, OutputStream out, long sleep, boolean closeInput, boolean closeOutput) {
+                       LangUtil.throwIaxIfNull(in, "in");
+                       LangUtil.throwIaxIfNull(out, "out");
+                       this.in = in;
+                       this.out = out;
+                       this.closeInput = closeInput;
+                       this.closeOutput = closeOutput;
+                       this.sleep = Math.min(0l, Math.max(60l * 1000l, sleep));
+               }
+
+               public void setSnoop(ByteArrayOutputStream snoop) {
+                       this.snoop = snoop;
+               }
+
+               /**
+                * Run the pipe. This halts on the first Throwable thrown or when a read returns -1 (for end-of-file) or on demand.
+                */
+               public void run() {
+                       totalWritten = 0;
+                       if (halt) {
+                               return;
+                       }
+                       try {
+                               final int MAX = 4096;
+                               byte[] buf = new byte[MAX];
+                               // TODO this blocks, hanging the harness
+                               int count = in.read(buf, 0, MAX);
+                               ByteArrayOutputStream mySnoop;
+                               while ((halt && finishStream && (0 < count)) || (!halt && (-1 != count))) {
+                                       out.write(buf, 0, count);
+                                       mySnoop = snoop;
+                                       if (null != mySnoop) {
+                                               mySnoop.write(buf, 0, count);
+                                       }
+                                       totalWritten += count;
+                                       if (halt && !finishStream) {
+                                               break;
+                                       }
+                                       if (!halt && (0 < sleep)) {
+                                               Thread.sleep(sleep);
+                                       }
+                                       if (halt && !finishStream) {
+                                               break;
+                                       }
+                                       count = in.read(buf, 0, MAX);
+                               }
+                       } catch (Throwable e) {
+                               thrown = e;
+                       } finally {
+                               halt = true;
+                               if (closeInput) {
+                                       try {
+                                               in.close();
+                                       } catch (IOException e) {
+                                               // ignore
+                                       }
+                               }
+                               if (closeOutput) {
+                                       try {
+                                               out.close();
+                                       } catch (IOException e) {
+                                               // ignore
+                                       }
+                               }
+                               done = true;
+                               completing(totalWritten, thrown);
+                       }
+
+               }
+
+               /**
+                * Tell the pipe to halt the next time it gains control.
+                * 
+                * @param wait if true, this waits synchronously until pipe is done
+                * @param finishStream if true, then continue until a read from the input stream returns no bytes, then halt.
+                * @return true if <code>run()</code> will return the next time it gains control
+                */
+               public boolean halt(boolean wait, boolean finishStream) {
+                       if (!halt) {
+                               halt = true;
+                       }
+                       if (wait) {
+                               while (!done) {
+                                       synchronized (this) {
+                                               notifyAll();
+                                       }
+                                       if (!done) {
+                                               try {
+                                                       Thread.sleep(5l);
+                                               } catch (InterruptedException e) {
+                                                       break;
+                                               }
+                                       }
+                               }
+                       }
+                       return halt;
+               }
+
+               /** @return the total number of bytes written */
+               public long totalWritten() {
+                       return totalWritten;
+               }
+
+               /** @return any exception thrown when reading/writing */
+               public Throwable getThrown() {
+                       return thrown;
+               }
+
+               /**
+                * This is called when the pipe is completing. This implementation does nothing. Subclasses implement this to get notice.
+                * Note that halt(true, true) might or might not have completed before this method is called.
+                */
+               protected void completing(long totalWritten, Throwable thrown) {
+               }
+       }
 
 }
index 4bcac41981d7c4e492486c5133244ff205fce019..c17ceaa40617041390093401a9df92c734b17160 100644 (file)
@@ -26,7 +26,6 @@ import java.util.Arrays;
 import java.util.BitSet;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.StringTokenizer;
@@ -34,270 +33,282 @@ import java.util.StringTokenizer;
 /**
  * 
  */
-public class LangUtil {    
-
-//     /** map from String version to String class implemented in that version or later */
-//    private static final Map VM_CLASSES;
-
-    public static final String EOL;
-    static {
-        StringWriter buf = new StringWriter();
-        PrintWriter writer = new PrintWriter(buf);
-        writer.println("");
-        String eol = "\n";
-        try { 
-            buf.close(); 
-            StringBuffer sb = buf.getBuffer(); 
-            if (sb!=null) {
-                eol = buf.toString();
-            }
-        } catch (Throwable t) { }
-        EOL = eol;
-        
-//        HashMap map = new HashMap();
-//        map.put("1.2", "java.lang.ref.Reference");
-//        map.put("1.3", "java.lang.reflect.Proxy");
-//        map.put("1.4", "java.nio.Buffer");
-//        map.put("1.5", "java.lang.annotation.Annotation");
-//        
-//        VM_CLASSES = Collections.unmodifiableMap(map);
-    }
-
-//    /**
-//     * Detect whether Java version is supported.
-//     * @param version String "1.2" or "1.3" or "1.4"
-//     * @return true if the currently-running VM supports the version 
-//     * @throws IllegalArgumentException if version is not known
-//     */
-//    public static final boolean supportsJava(String version) {
-//        LangUtil.throwIaxIfNull(version, "version");
-//        String className = (String) VM_CLASSES.get(version);
-//        if (null == className) {
-//            throw new IllegalArgumentException("unknown version: " + version);
-//        }
-//        try {
-//            Class.forName(className);
-//            return true;
-//        } catch (Throwable t) {
-//            return false;
-//        }        
-//    }
-       
+public class LangUtil {
+
+       // /** map from String version to String class implemented in that version or later */
+       // private static final Map VM_CLASSES;
+
+       public static final String EOL;
+       static {
+               StringWriter buf = new StringWriter();
+               PrintWriter writer = new PrintWriter(buf);
+               writer.println("");
+               String eol = "\n";
+               try {
+                       buf.close();
+                       StringBuffer sb = buf.getBuffer();
+                       if (sb != null) {
+                               eol = buf.toString();
+                       }
+               } catch (Throwable t) {
+               }
+               EOL = eol;
+
+               // HashMap map = new HashMap();
+               // map.put("1.2", "java.lang.ref.Reference");
+               // map.put("1.3", "java.lang.reflect.Proxy");
+               // map.put("1.4", "java.nio.Buffer");
+               // map.put("1.5", "java.lang.annotation.Annotation");
+               //        
+               // VM_CLASSES = Collections.unmodifiableMap(map);
+       }
+
+       // /**
+       // * Detect whether Java version is supported.
+       // * @param version String "1.2" or "1.3" or "1.4"
+       // * @return true if the currently-running VM supports the version
+       // * @throws IllegalArgumentException if version is not known
+       // */
+       // public static final boolean supportsJava(String version) {
+       // LangUtil.throwIaxIfNull(version, "version");
+       // String className = (String) VM_CLASSES.get(version);
+       // if (null == className) {
+       // throw new IllegalArgumentException("unknown version: " + version);
+       // }
+       // try {
+       // Class.forName(className);
+       // return true;
+       // } catch (Throwable t) {
+       // return false;
+       // }
+       // }
+
        private static boolean is13VMOrGreater = true;
        private static boolean is14VMOrGreater = true;
        private static boolean is15VMOrGreater = false;
        private static boolean is16VMOrGreater = false;
-       
+
        static {
-        String vm = System.getProperty("java.version"); // JLS 20.18.7
-        if (vm==null) vm = System.getProperty("java.runtime.version");
-               if (vm==null) vm = System.getProperty("java.vm.version");
+               String vm = System.getProperty("java.version"); // JLS 20.18.7
+               if (vm == null)
+                       vm = System.getProperty("java.runtime.version");
+               if (vm == null)
+                       vm = System.getProperty("java.vm.version");
                if (vm.startsWith("1.3")) {
                        is14VMOrGreater = false;
-               } else if (vm.startsWith("1.5") || vm.startsWith("1.6")) {      
+               } else if (vm.startsWith("1.5") || vm.startsWith("1.6")) {
                        is15VMOrGreater = true;
                        is16VMOrGreater = true;
                }
        }
-       
-       public static boolean is13VMOrGreater() { return is13VMOrGreater;}
-       public static boolean is14VMOrGreater() { return is14VMOrGreater;}
-       public static boolean is15VMOrGreater() { return is15VMOrGreater;}
-       public static boolean is16VMOrGreater() { return is16VMOrGreater;}
-    
-    /**
-     * Shorthand for "if null, throw IllegalArgumentException"
-     * @throws IllegalArgumentException "null {name}" if o is null 
-     */
-    public static final void throwIaxIfNull(final Object o, final String name) {
-        if (null == o) {
-            String message = "null " + (null == name ? "input" : name);
-            throw new IllegalArgumentException(message);
-        }
-    }
-    
-    /**
-     * Shorthand for "if not null or not assignable, throw IllegalArgumentException"
-     * @param c the Class to check - use null to ignore type check
-     * @throws IllegalArgumentException "null {name}" if o is null 
-     */
-    public static final void throwIaxIfNotAssignable(final Object ra[], final Class c, final String name) {
-        throwIaxIfNull(ra, name);
-        String label = (null == name ? "input" : name);
-        for (int i = 0; i < ra.length; i++) {
-            if (null == ra[i]) {
-                String m = " null " + label + "[" + i + "]";
-                throw new IllegalArgumentException(m);
-            } else if (null != c) {
-                Class actualClass = ra[i].getClass();
-                if (!c.isAssignableFrom(actualClass)) {
-                    String message = label + " not assignable to " + c.getName();
-                    throw new IllegalArgumentException(message);           
-                }
-            }
-        }
-    }
-    /**
-     * Shorthand for "if not null or not assignable, throw IllegalArgumentException"
-     * @throws IllegalArgumentException "null {name}" if o is null 
-     */
-    public static final void throwIaxIfNotAssignable(final Object o, final Class c, final String name) {
-        throwIaxIfNull(o, name);
-        if (null != c) {
-            Class actualClass = o.getClass();
-            if (!c.isAssignableFrom(actualClass)) {
-                String message = name + " not assignable to " + c.getName();
-                throw new IllegalArgumentException(message);           
-            }
-        }
-    }
-    
-//    /**
-//     * Shorthand for "if any not null or not assignable, throw IllegalArgumentException"
-//     * @throws IllegalArgumentException "{name} is not assignable to {c}"  
-//     */
-//    public static final void throwIaxIfNotAllAssignable(final Collection collection, 
-//        final Class c, final String name) {
-//        throwIaxIfNull(collection, name);
-//        if (null != c) {
-//            for (Iterator iter = collection.iterator(); iter.hasNext();) {
-//                             throwIaxIfNotAssignable(iter.next(), c, name);
-//                             
-//                     }
-//        }
-//    }
-    /**
-     * Shorthand for "if false, throw IllegalArgumentException"
-     * @throws IllegalArgumentException "{message}" if test is false
-     */
-    public static final void throwIaxIfFalse(final boolean test, final String message) {
-        if (!test) {
-            throw new IllegalArgumentException(message);
-        }
-    }
-    
-//    /** @return ((null == s) || (0 == s.trim().length())); */
-//    public static boolean isEmptyTrimmed(String s) {
-//        return ((null == s) || (0 == s.length())
-//            || (0 == s.trim().length()));
-//    }
-
-    /** @return ((null == s) || (0 == s.length())); */
-    public static boolean isEmpty(String s) {
-        return ((null == s) || (0 == s.length()));
-    }
-
-    /** @return ((null == ra) || (0 == ra.length)) */
-    public static boolean isEmpty(Object[] ra) {
-        return ((null == ra) || (0 == ra.length));
-    }
-
-    /** @return ((null == collection) || (0 == collection.size())) */
-    public static boolean isEmpty(Collection collection) {
-        return ((null == collection) || (0 == collection.size()));
-    }
-
-    /**
+
+       public static boolean is13VMOrGreater() {
+               return is13VMOrGreater;
+       }
+
+       public static boolean is14VMOrGreater() {
+               return is14VMOrGreater;
+       }
+
+       public static boolean is15VMOrGreater() {
+               return is15VMOrGreater;
+       }
+
+       public static boolean is16VMOrGreater() {
+               return is16VMOrGreater;
+       }
+
+       /**
+        * Shorthand for "if null, throw IllegalArgumentException"
+        * 
+        * @throws IllegalArgumentException "null {name}" if o is null
+        */
+       public static final void throwIaxIfNull(final Object o, final String name) {
+               if (null == o) {
+                       String message = "null " + (null == name ? "input" : name);
+                       throw new IllegalArgumentException(message);
+               }
+       }
+
+       /**
+        * Shorthand for "if not null or not assignable, throw IllegalArgumentException"
+        * 
+        * @param c the Class to check - use null to ignore type check
+        * @throws IllegalArgumentException "null {name}" if o is null
+        */
+       public static final void throwIaxIfNotAssignable(final Object ra[], final Class c, final String name) {
+               throwIaxIfNull(ra, name);
+               String label = (null == name ? "input" : name);
+               for (int i = 0; i < ra.length; i++) {
+                       if (null == ra[i]) {
+                               String m = " null " + label + "[" + i + "]";
+                               throw new IllegalArgumentException(m);
+                       } else if (null != c) {
+                               Class actualClass = ra[i].getClass();
+                               if (!c.isAssignableFrom(actualClass)) {
+                                       String message = label + " not assignable to " + c.getName();
+                                       throw new IllegalArgumentException(message);
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Shorthand for "if not null or not assignable, throw IllegalArgumentException"
+        * 
+        * @throws IllegalArgumentException "null {name}" if o is null
+        */
+       public static final void throwIaxIfNotAssignable(final Object o, final Class c, final String name) {
+               throwIaxIfNull(o, name);
+               if (null != c) {
+                       Class actualClass = o.getClass();
+                       if (!c.isAssignableFrom(actualClass)) {
+                               String message = name + " not assignable to " + c.getName();
+                               throw new IllegalArgumentException(message);
+                       }
+               }
+       }
+
+       // /**
+       // * Shorthand for "if any not null or not assignable, throw IllegalArgumentException"
+       // * @throws IllegalArgumentException "{name} is not assignable to {c}"
+       // */
+       // public static final void throwIaxIfNotAllAssignable(final Collection collection,
+       // final Class c, final String name) {
+       // throwIaxIfNull(collection, name);
+       // if (null != c) {
+       // for (Iterator iter = collection.iterator(); iter.hasNext();) {
+       // throwIaxIfNotAssignable(iter.next(), c, name);
+       //                              
+       // }
+       // }
+       // }
+       /**
+        * Shorthand for "if false, throw IllegalArgumentException"
+        * 
+        * @throws IllegalArgumentException "{message}" if test is false
+        */
+       public static final void throwIaxIfFalse(final boolean test, final String message) {
+               if (!test) {
+                       throw new IllegalArgumentException(message);
+               }
+       }
+
+       // /** @return ((null == s) || (0 == s.trim().length())); */
+       // public static boolean isEmptyTrimmed(String s) {
+       // return ((null == s) || (0 == s.length())
+       // || (0 == s.trim().length()));
+       // }
+
+       /** @return ((null == s) || (0 == s.length())); */
+       public static boolean isEmpty(String s) {
+               return ((null == s) || (0 == s.length()));
+       }
+
+       /** @return ((null == ra) || (0 == ra.length)) */
+       public static boolean isEmpty(Object[] ra) {
+               return ((null == ra) || (0 == ra.length));
+       }
+
+       /** @return ((null == collection) || (0 == collection.size())) */
+       public static boolean isEmpty(Collection collection) {
+               return ((null == collection) || (0 == collection.size()));
+       }
+
+       /**
         * Splits <code>text</code> at whitespace.
-        *
+        * 
         * @param text <code>String</code> to split.
         */
        public static String[] split(String text) {
                return (String[]) strings(text).toArray(new String[0]);
        }
-    
-    /**
-     * Splits <code>input</code> at commas, 
-     * trimming any white space.
-     *
-     * @param input <code>String</code> to split.
-     * @return List of String of elements.
-     */
-    public static List commaSplit(String input) {
-        return anySplit(input, ",");
-    }
-    
-    /**
-     * Split string as classpath, delimited at File.pathSeparator.
-     * Entries are not trimmed, but empty entries are ignored.
-     * @param classpath the String to split - may be null or empty
-     * @return String[] of classpath entries
-     */
-    public static String[] splitClasspath(String classpath) {
-        if (LangUtil.isEmpty(classpath)) {
-            return new String[0];
-        }
-        StringTokenizer st = new StringTokenizer(classpath, File.pathSeparator);
-        ArrayList result = new ArrayList(st.countTokens());
-        while (st.hasMoreTokens()) {
-            String entry = st.nextToken();
-            if (!LangUtil.isEmpty(entry)) {
-                result.add(entry);
-            }
-        }
-        return (String[]) result.toArray(new String[0]);
-    }
-
-    /** 
-     * Get System property as boolean, 
-     * but use default value where the system property is not set.
-     * @return true if value is set to true, false otherwise 
-     */
-    public static boolean getBoolean(String propertyName, boolean defaultValue) {
-        if (null != propertyName) {
-            try {
-                String value = System.getProperty(propertyName);
-                if (null != value) {
-                    return Boolean.valueOf(value).booleanValue();
-                }
-            } catch (Throwable t) {
-                // default below
-            }
-        }
-        return defaultValue;
-    }
-
-    /**
-     * Splits <code>input</code>, removing delimiter and 
-     * trimming any white space.
-     * Returns an empty collection if the input is null.
-     * If delimiter is null or empty or if the input contains
-     * no delimiters, the input itself is returned
-     * after trimming white space.
-     *
-     * @param input <code>String</code> to split.
-     * @param delim <code>String</code> separators for input.
-     * @return List of String of elements.
-     */
-    public static List anySplit(String input, String delim) {
-        if (null == input) {
-            return Collections.EMPTY_LIST;
-        } 
-        ArrayList result = new ArrayList();
-        
-        if (LangUtil.isEmpty(delim)
-            || (-1 == input.indexOf(delim))) {
-            result.add(input.trim());
-        } else {
-            StringTokenizer st = new StringTokenizer(input, delim);
-            while (st.hasMoreTokens()) {
-                result.add(st.nextToken().trim());
-            }
-        }
-        return result;        
-    }
-    
 
        /**
-        * Splits strings into a <code>List</code> using a
-        * <code>StringTokenizer</code>.
-        *
+        * Splits <code>input</code> at commas, trimming any white space.
+        * 
+        * @param input <code>String</code> to split.
+        * @return List of String of elements.
+        */
+       public static List commaSplit(String input) {
+               return anySplit(input, ",");
+       }
+
+       /**
+        * Split string as classpath, delimited at File.pathSeparator. Entries are not trimmed, but empty entries are ignored.
+        * 
+        * @param classpath the String to split - may be null or empty
+        * @return String[] of classpath entries
+        */
+       public static String[] splitClasspath(String classpath) {
+               if (LangUtil.isEmpty(classpath)) {
+                       return new String[0];
+               }
+               StringTokenizer st = new StringTokenizer(classpath, File.pathSeparator);
+               ArrayList result = new ArrayList(st.countTokens());
+               while (st.hasMoreTokens()) {
+                       String entry = st.nextToken();
+                       if (!LangUtil.isEmpty(entry)) {
+                               result.add(entry);
+                       }
+               }
+               return (String[]) result.toArray(new String[0]);
+       }
+
+       /**
+        * Get System property as boolean, but use default value where the system property is not set.
+        * 
+        * @return true if value is set to true, false otherwise
+        */
+       public static boolean getBoolean(String propertyName, boolean defaultValue) {
+               if (null != propertyName) {
+                       try {
+                               String value = System.getProperty(propertyName);
+                               if (null != value) {
+                                       return Boolean.valueOf(value).booleanValue();
+                               }
+                       } catch (Throwable t) {
+                               // default below
+                       }
+               }
+               return defaultValue;
+       }
+
+       /**
+        * Splits <code>input</code>, removing delimiter and trimming any white space. Returns an empty collection if the input is null.
+        * If delimiter is null or empty or if the input contains no delimiters, the input itself is returned after trimming white
+        * space.
+        * 
+        * @param input <code>String</code> to split.
+        * @param delim <code>String</code> separators for input.
+        * @return List of String of elements.
+        */
+       public static List anySplit(String input, String delim) {
+               if (null == input) {
+                       return Collections.EMPTY_LIST;
+               }
+               ArrayList result = new ArrayList();
+
+               if (LangUtil.isEmpty(delim) || (-1 == input.indexOf(delim))) {
+                       result.add(input.trim());
+               } else {
+                       StringTokenizer st = new StringTokenizer(input, delim);
+                       while (st.hasMoreTokens()) {
+                               result.add(st.nextToken().trim());
+                       }
+               }
+               return result;
+       }
+
+       /**
+        * Splits strings into a <code>List</code> using a <code>StringTokenizer</code>.
+        * 
         * @param text <code>String</code> to split.
         */
        public static List strings(String text) {
-        if (LangUtil.isEmpty(text)) {
-            return Collections.EMPTY_LIST;
-        }
+               if (LangUtil.isEmpty(text)) {
+                       return Collections.EMPTY_LIST;
+               }
                List strings = new ArrayList();
                StringTokenizer tok = new StringTokenizer(text);
                while (tok.hasMoreTokens()) {
@@ -308,1169 +319,1119 @@ public class LangUtil {
 
        /** @return a non-null unmodifiable List */
        public static List safeList(List list) {
-               return (
-                       null == list
-                               ? Collections.EMPTY_LIST
-                               : Collections.unmodifiableList(list));
+               return (null == list ? Collections.EMPTY_LIST : Collections.unmodifiableList(list));
+       }
+
+       // /**
+       // * Select from input String[] based on suffix-matching
+       // * @param inputs String[] of input - null ignored
+       // * @param suffixes String[] of suffix selectors - null ignored
+       // * @param ignoreCase if true, ignore case
+       // * @return String[] of input that end with any input
+       // */
+       // public static String[] endsWith(String[] inputs, String[] suffixes, boolean ignoreCase) {
+       // if (LangUtil.isEmpty(inputs) || LangUtil.isEmpty(suffixes)) {
+       // return new String[0];
+       // }
+       // if (ignoreCase) {
+       // String[] temp = new String[suffixes.length];
+       // for (int i = 0; i < temp.length; i++) {
+       // String suff = suffixes[i];
+       // temp[i] = (null == suff ? null : suff.toLowerCase());
+       // }
+       // suffixes = temp;
+       // }
+       // ArrayList result = new ArrayList();
+       // for (int i = 0; i < inputs.length; i++) {
+       // String input = inputs[i];
+       // if (null == input) {
+       // continue;
+       // }
+       // if (!ignoreCase) {
+       // input = input.toLowerCase();
+       // }
+       // for (int j = 0; j < suffixes.length; j++) {
+       // String suffix = suffixes[j];
+       // if (null == suffix) {
+       // continue;
+       // }
+       // if (input.endsWith(suffix)) {
+       // result.add(input);
+       // break;
+       // }
+       // }
+       // }
+       // return (String[]) result.toArray(new String[0]);
+       // }
+       //    
+       // /**
+       // * Select from input String[] if readable directories
+       // * @param inputs String[] of input - null ignored
+       // * @param baseDir the base directory of the input
+       // * @return String[] of input that end with any input
+       // */
+       // public static String[] selectDirectories(String[] inputs, File baseDir) {
+       // if (LangUtil.isEmpty(inputs)) {
+       // return new String[0];
+       // }
+       // ArrayList result = new ArrayList();
+       // for (int i = 0; i < inputs.length; i++) {
+       // String input = inputs[i];
+       // if (null == input) {
+       // continue;
+       // }
+       // File inputFile = new File(baseDir, input);
+       // if (inputFile.canRead() && inputFile.isDirectory()) {
+       // result.add(input);
+       // }
+       // }
+       // return (String[]) result.toArray(new String[0]);
+       // }
+
+       /**
+        * copy non-null two-dimensional String[][]
+        * 
+        * @see extractOptions(String[], String[][])
+        */
+       public static String[][] copyStrings(String[][] in) {
+               String[][] out = new String[in.length][];
+               for (int i = 0; i < out.length; i++) {
+                       out[i] = new String[in[i].length];
+                       System.arraycopy(in[i], 0, out[i], 0, out[i].length);
+               }
+               return out;
        }
-    
-//    /**
-//     * Select from input String[] based on suffix-matching
-//     * @param inputs String[] of input - null ignored
-//     * @param suffixes String[] of suffix selectors - null ignored
-//     * @param ignoreCase if true, ignore case
-//     * @return String[] of input that end with any input
-//     */
-//    public static String[] endsWith(String[] inputs, String[] suffixes, boolean ignoreCase) {
-//        if (LangUtil.isEmpty(inputs) || LangUtil.isEmpty(suffixes)) {
-//            return new String[0];
-//        }
-//        if (ignoreCase) {
-//            String[] temp = new String[suffixes.length];
-//            for (int i = 0; i < temp.length; i++) {                
-//                             String suff = suffixes[i];
-//                temp[i] = (null ==  suff ? null : suff.toLowerCase());
-//                     }
-//            suffixes = temp;
-//        }
-//        ArrayList result = new ArrayList();
-//        for (int i = 0; i < inputs.length; i++) {
-//            String input = inputs[i];
-//            if (null == input) {
-//                continue;
-//            }
-//            if (!ignoreCase) {
-//                input = input.toLowerCase();
-//            }
-//            for (int j = 0; j < suffixes.length; j++) {
-//                String suffix = suffixes[j];
-//                if (null == suffix) {
-//                    continue;
-//                }
-//                if (input.endsWith(suffix)) {
-//                    result.add(input);
-//                    break;
-//                }
-//            }
-//        }
-//        return (String[]) result.toArray(new String[0]);
-//    }
-//    
-//    /**
-//     * Select from input String[] if readable directories
-//     * @param inputs String[] of input - null ignored
-//     * @param baseDir the base directory of the input
-//     * @return String[] of input that end with any input
-//     */
-//    public static String[] selectDirectories(String[] inputs, File baseDir) {
-//        if (LangUtil.isEmpty(inputs)) {
-//            return new String[0];
-//        }
-//        ArrayList result = new ArrayList();
-//        for (int i = 0; i < inputs.length; i++) {
-//            String input = inputs[i];
-//            if (null == input) {
-//                continue;
-//            }
-//            File inputFile = new File(baseDir, input);
-//            if (inputFile.canRead() && inputFile.isDirectory()) {
-//                result.add(input);
-//            }
-//        }
-//        return (String[]) result.toArray(new String[0]);
-//    }
-
-    /** 
-     * copy non-null two-dimensional String[][] 
-     * @see extractOptions(String[], String[][]) 
-     */
-    public static String[][] copyStrings(String[][] in) {
-        String[][] out = new String[in.length][];
-        for (int i = 0; i < out.length; i++) {
-            out[i] = new String[in[i].length];
-            System.arraycopy(in[i], 0, out[i], 0, out[i].length);
-        } 
-        return out;
-    }
-
-    /** 
-     * Extract options and arguments to input option list, returning remainder.
-     * The input options will be nullified if not found.  e.g., 
-     * <pre>String[] options = new String[][] { new String[] { "-verbose" },
-     *     new String[] { "-classpath", null } };
-     * String[] args = extractOptions(args, options);
-     * boolean verbose = null != options[0][0];
-     * boolean classpath = options[1][1];</pre>
-     * @param args the String[] input options
-     * @param options the String[][]options to find in the input args - not null
-     *         for each String[] component the first subcomponent is the option itself,
-     *         and there is one String subcomponent for each additional argument.
-     * @return String[] of args remaining after extracting options to extracted 
-     */
-    public static String[] extractOptions(String[] args, String[][] options) {
-        if (LangUtil.isEmpty(args) || LangUtil.isEmpty(options) ) {
-            return args;
-        }
-        BitSet foundSet = new BitSet();
-        String[] result = new String[args.length];
-        int resultIndex = 0;
-        for (int j = 0; j < args.length; j++) {
-            boolean found = false;
-            for (int i = 0; !found && (i < options.length); i++) {
-                String[] option = options[i];
-                LangUtil.throwIaxIfFalse(!LangUtil.isEmpty(option), "options");
-                String sought = option[0];
-                found = sought.equals(args[j]);
-                if (found) {                    
-                    foundSet.set(i);
-                    int doMore = option.length-1;
-                    if (0 < doMore) {
-                        final int MAX = j + doMore;
-                        if (MAX >= args.length) {
-                            String s = "expecting " + doMore + " args after ";
-                            throw new IllegalArgumentException(s + args[j]);
-                        }
-                        for (int k = 1; k < option.length; k++) {
+
+       /**
+        * Extract options and arguments to input option list, returning remainder. The input options will be nullified if not found.
+        * e.g.,
+        * 
+        * <pre>
+        * String[] options = new String[][] { new String[] { &quot;-verbose&quot; }, new String[] { &quot;-classpath&quot;, null } };
+        * String[] args = extractOptions(args, options);
+        * boolean verbose = null != options[0][0];
+        * boolean classpath = options[1][1];
+        * </pre>
+        * 
+        * @param args the String[] input options
+        * @param options the String[][]options to find in the input args - not null for each String[] component the first subcomponent
+        *        is the option itself, and there is one String subcomponent for each additional argument.
+        * @return String[] of args remaining after extracting options to extracted
+        */
+       public static String[] extractOptions(String[] args, String[][] options) {
+               if (LangUtil.isEmpty(args) || LangUtil.isEmpty(options)) {
+                       return args;
+               }
+               BitSet foundSet = new BitSet();
+               String[] result = new String[args.length];
+               int resultIndex = 0;
+               for (int j = 0; j < args.length; j++) {
+                       boolean found = false;
+                       for (int i = 0; !found && (i < options.length); i++) {
+                               String[] option = options[i];
+                               LangUtil.throwIaxIfFalse(!LangUtil.isEmpty(option), "options");
+                               String sought = option[0];
+                               found = sought.equals(args[j]);
+                               if (found) {
+                                       foundSet.set(i);
+                                       int doMore = option.length - 1;
+                                       if (0 < doMore) {
+                                               final int MAX = j + doMore;
+                                               if (MAX >= args.length) {
+                                                       String s = "expecting " + doMore + " args after ";
+                                                       throw new IllegalArgumentException(s + args[j]);
+                                               }
+                                               for (int k = 1; k < option.length; k++) {
                                                        option[k] = args[++j];
                                                }
-                    }
-                }
-            }
-            if (!found) {
-                result[resultIndex++] = args[j];
-            }
-        }
-        
-        // unset any not found
-        for (int i = 0; i < options.length; i++) {
-            if (!foundSet.get(i)) {
-                options[i][0] = null;
-            }
-        }        
-        // fixup remainder
-        if (resultIndex < args.length) {
-            String[] temp = new String[resultIndex];
-            System.arraycopy(result, 0, temp, 0, resultIndex);
-            args = temp;
-        }
-        
-        return args;        
-    }
-//    
-//    /** 
-//     * Extract options and arguments to input parameter list, returning remainder.
-//     * @param args the String[] input options
-//     * @param validOptions the String[] options to find in the input args - not null
-//     * @param optionArgs the int[] number of arguments for each option in validOptions
-//     *         (if null, then no arguments for any option)
-//     * @param extracted the List for the matched options
-//     * @return String[] of args remaining after extracting options to extracted 
-//     */
-//    public static String[] extractOptions(String[] args, String[] validOptions,
-//        int[] optionArgs, List extracted) {
-//        if (LangUtil.isEmpty(args) 
-//            || LangUtil.isEmpty(validOptions) ) {
-//            return args;
-//        }
-//        if (null != optionArgs) {
-//            if (optionArgs.length != validOptions.length) {
-//                throw new IllegalArgumentException("args must match options");
-//            }
-//        }
-//        String[] result = new String[args.length];
-//        int resultIndex = 0;
-//        for (int j = 0; j < args.length; j++) {
-//            boolean found = false;
-//            for (int i = 0; !found && (i < validOptions.length); i++) {
-//                String sought = validOptions[i];
-//                int doMore = (null == optionArgs ? 0 : optionArgs[i]);
-//                if (LangUtil.isEmpty(sought)) {
-//                    continue;
-//                }
-//                found = sought.equals(args[j]);
-//                if (found) {                    
-//                    if (null != extracted) {
-//                        extracted.add(sought);
-//                    }
-//                    if (0 < doMore) {
-//                        final int MAX = j + doMore;
-//                        if (MAX >= args.length) {
-//                            String s = "expecting " + doMore + " args after ";
-//                            throw new IllegalArgumentException(s + args[j]);
-//                        }
-//                        if (null != extracted) {                            
-//                            while (j < MAX) {
-//                                extracted.add(args[++j]);
-//                            }
-//                        } else {
-//                            j = MAX;
-//                        }                        
-//                    }
-//                    break;
-//                }
-//            }
-//            if (!found) {
-//                result[resultIndex++] = args[j];
-//            }
-//        }
-//        if (resultIndex < args.length) {
-//            String[] temp = new String[resultIndex];
-//            System.arraycopy(result, 0, temp, 0, resultIndex);
-//            args = temp;
-//        }
-//        return args;        
-//    }
-
-//    /** @return String[] of entries in validOptions found in args */
-//    public static String[] selectOptions(String[] args, String[] validOptions) {
-//        if (LangUtil.isEmpty(args) || LangUtil.isEmpty(validOptions)) {
-//            return new String[0];
-//        }
-//        ArrayList result = new ArrayList();
-//        for (int i = 0; i < validOptions.length; i++) {
-//            String sought = validOptions[i];
-//            if (LangUtil.isEmpty(sought)) {
-//                continue;
-//            }
-//                     for (int j = 0; j < args.length; j++) {
-//                             if (sought.equals(args[j])) {
-//                    result.add(sought);
-//                    break;
-//                }
-//                     }
-//             }
-//        return (String[]) result.toArray(new String[0]);
-//    }
-    
-//    /** @return String[] of entries in validOptions found in args */
-//    public static String[] selectOptions(List args, String[] validOptions) {
-//        if (LangUtil.isEmpty(args) || LangUtil.isEmpty(validOptions)) {
-//            return new String[0];
-//        }
-//        ArrayList result = new ArrayList();
-//        for (int i = 0; i < validOptions.length; i++) {
-//            String sought = validOptions[i];
-//            if (LangUtil.isEmpty(sought)) {
-//                continue;
-//            }
-//            for (Iterator iter = args.iterator(); iter.hasNext();) {
-//                             String arg = (String) iter.next();
-//                if (sought.equals(arg)) {
-//                    result.add(sought);
-//                    break;
-//                }
-//            }
-//        }
-//        return (String[]) result.toArray(new String[0]);
-//    }
-    
-//    /**
-//     * Generate variants of String[] options by creating an extra set for
-//     * each option that ends with "-".  If none end with "-", then an
-//     * array equal to <code>new String[][] { options }</code> is returned;
-//     * if one ends with "-", then two sets are returned,
-//     * three causes eight sets, etc.
-//     * @return String[][] with each option set.
-//     * @throws IllegalArgumentException if any option is null or empty.
-//     */
-//    public static String[][] optionVariants(String[] options) {
-//        if ((null == options) || (0 == options.length)) {
-//            return new String[][] { new String[0]};            
-//        }
-//        // be nice, don't stomp input
-//        String[] temp = new String[options.length];
-//        System.arraycopy(options, 0, temp, 0, temp.length);
-//        options = temp;
-//        boolean[] dup = new boolean[options.length];
-//        int numDups = 0;
-//        
-//        for (int i = 0; i < options.length; i++) {
-//            String option = options[i];
-//            if (LangUtil.isEmpty(option)) {
-//                throw new IllegalArgumentException("empty option at " + i);
-//            }
-//            if (option.endsWith("-")) {
-//                options[i] = option.substring(0, option.length()-1);
-//                dup[i] = true;
-//                numDups++;
-//            }
-//        }
-//        final String[] NONE = new String[0];
-//        final int variants = exp(2, numDups);
-//        final String[][] result = new String[variants][];
-//        // variant is a bitmap wrt doing extra value when dup[k]=true
-//        for (int variant = 0; variant < variants; variant++) { 
-//            ArrayList next = new ArrayList();
-//            int nextOption = 0;
-//            for (int k = 0; k < options.length; k++) {
-//                if (!dup[k] || (0 != (variant & (1 << (nextOption++))))) {
-//                    next.add(options[k]);
-//                }                   
-//            }
-//            result[variant] = (String[]) next.toArray(NONE);
-//        }
-//        return result;
-//    }
-//    
-//    private static int exp(int base, int power) { // not in Math?
-//        if (0 > power) {
-//            throw new IllegalArgumentException("negative power: " + power);
-//        } 
-//        int result = 1;
-//        while (0 < power--) {
-//            result *= base;
-//        }
-//        return result;
-//    }
-
-//    /**
-//     * Make a copy of the array.
-//     * @return an array with the same component type as source
-//     * containing same elements, even if null.
-//     * @throws IllegalArgumentException if source is null
-//     */
-//    public static final Object[] copy(Object[] source) {
-//        LangUtil.throwIaxIfNull(source, "source");        
-//        final Class c = source.getClass().getComponentType();
-//        Object[] result = (Object[]) Array.newInstance(c, source.length);
-//        System.arraycopy(source, 0, result, 0, result.length);
-//        return result;
-//    }
-    
-    
-    /**
-     * Convert arrays safely.  The number of elements in the result
-     * will be 1 smaller for each element that is null or not assignable.
-     * This will use sink if it has exactly the right size.
-     * The result will always have the same component type as sink.
-     * @return an array with the same component type as sink
-     * containing any assignable elements in source (in the same order).
-     * @throws IllegalArgumentException if either is null
-     */
-    public static Object[] safeCopy(Object[] source, Object[] sink) {
-        final Class sinkType = (null == sink 
-                                ? Object.class 
-                                : sink.getClass().getComponentType());
-        final int sourceLength = (null == source ? 0 : source.length);
-        final int sinkLength = (null == sink ? 0 : sink.length);
-        
-        final int resultSize;
-        ArrayList result = null;
-        if (0 == sourceLength) {
-            resultSize = 0;
-        } else {
-            result = new ArrayList(sourceLength);
-            for (int i = 0; i < sourceLength; i++) {
-                if ((null != source[i])
-                    && (sinkType.isAssignableFrom(source[i].getClass()))) {
-                    result.add(source[i]);
-                }
-            }
-            resultSize = result.size();
-        }
-        if (resultSize != sinkLength) {
-            sink = (Object[]) Array.newInstance(sinkType, result.size());
-        }
-        if (0 < resultSize) {
-            sink = result.toArray(sink);
-        }
-        return sink;
-    }
-
-       /** 
-     * @return a String with the unqualified class name of the class (or "null")
-     */
-    public static String unqualifiedClassName(Class c) {
-        if (null == c) {
-            return "null";
-        }
-        String name = c.getName();
-        int loc = name.lastIndexOf(".");
-        if (-1 != loc) {
-            name = name.substring(1 + loc);
-        }
-        return name;
-    }
-
-       /** 
-     * @return a String with the unqualified class name of the object (or "null")
-     */
-    public static String unqualifiedClassName(Object o) {
-        return LangUtil.unqualifiedClassName(null == o ? null : o.getClass());
-    }
-
-    /** inefficient way to replace all instances of sought with replace */
-    public static String replace(String in, String sought, String replace) {
-        if (LangUtil.isEmpty(in) || LangUtil.isEmpty(sought)) {
-            return in;
-        }
-        StringBuffer result = new StringBuffer();
-        final int len = sought.length();
-        int start = 0;
-        int loc;
-        while (-1 != (loc = in.indexOf(sought, start))) {
-            result.append(in.substring(start, loc));
-            if (!LangUtil.isEmpty(replace)) {
-                result.append(replace);
-            }
-            start = loc + len;
-        }
-        result.append(in.substring(start));
-        return result.toString();    
-    }
-    
-    /** render i right-justified with a given width less than about 40 */
-    public static String toSizedString(long i, int width) {
-        String result = "" + i;
-        int size = result.length();
-        if (width > size) {
-            final String pad = "                                              ";
-            final int padLength = pad.length();
-            if (width > padLength) {
-                width = padLength;
-            }
-            int topad = width-size;
-            result = pad.substring(0, topad) + result;
-        }
-        return result;
-    }
-
-//    /** clip StringBuffer to maximum number of lines */
-//    static String clipBuffer(StringBuffer buffer, int maxLines) {
-//        if ((null == buffer) || (1 > buffer.length())) return "";
-//        StringBuffer result = new StringBuffer();
-//        int j = 0;
-//        final int MAX = maxLines;
-//        final int N = buffer.length();
-//        for (int i = 0, srcBegin = 0; i < MAX; srcBegin += j) {
-//            // todo: replace with String variant if/since getting char?
-//            char[] chars = new char[128];
-//            int srcEnd = srcBegin+chars.length;
-//            if (srcEnd >= N) {
-//                srcEnd = N-1;
-//            }
-//            if (srcBegin == srcEnd) break;
-//            //log("srcBegin:" + srcBegin + ":srcEnd:" + srcEnd);
-//            buffer.getChars(srcBegin, srcEnd, chars, 0);            
-//            for (j = 0; j < srcEnd-srcBegin/*chars.length*/; j++) {
-//                char c = chars[j];
-//                if (c == '\n') {
-//                    i++;
-//                    j++;
-//                    break;
-//                }
-//            }
-//            try { result.append(chars, 0, j); } 
-//            catch (Throwable t) { }
-//        }
-//        return result.toString();
-//    }
-
-    /**
-     * @return "({UnqualifiedExceptionClass}) {message}"
-     */
-    public static String renderExceptionShort(Throwable e) {
-        if (null == e)
-            return "(Throwable) null";
-        return "(" + LangUtil.unqualifiedClassName(e) + ") " + e.getMessage();
-    }
-
-   /**
-     * Renders exception <code>t</code> after unwrapping and 
-     * eliding any test packages.
-     * @param t <code>Throwable</code> to print.
-     * @see   #maxStackTrace
-     */
-    public static String renderException(Throwable t) { 
-        return renderException(t, true);
-    }
-    
-   /**
-     * Renders exception <code>t</code>, unwrapping,
-     * optionally eliding and limiting total number of lines.
-     * @param t <code>Throwable</code> to print.
-     * @param elide true to limit to 100 lines and elide test packages
-     * @see StringChecker#TEST_PACKAGES
-     */
-    public static String renderException(Throwable t, boolean elide) {
-        if (null == t) return "null throwable";
-        t = unwrapException(t);
-        StringBuffer stack = stackToString(t, false);
-        if (elide) {
-            elideEndingLines(StringChecker.TEST_PACKAGES, stack, 100);
-        }
-        return stack.toString();
-    }
-
-    /**
-     * Trim ending lines from a StringBuffer,
-     * clipping to maxLines and further removing any number of
-     * trailing lines accepted by checker.
-     * @param checker returns true if trailing line should be elided.
-     * @param stack StringBuffer with lines to elide
-     * @param maxLines int for maximum number of resulting lines
-     */
-    static void elideEndingLines(StringChecker checker, StringBuffer stack, int maxLines) {
-        if (null == checker || (null == stack) || (0 == stack.length())) {
-            return;
-        }
-        final LinkedList lines = new LinkedList();
-        StringTokenizer st = new StringTokenizer(stack.toString(),"\n\r");
-        while (st.hasMoreTokens() && (0 < --maxLines)) {
-            lines.add(st.nextToken());
-        }
-        st = null;
-        
-        String line;
-        int elided = 0;
-        while (!lines.isEmpty()) { 
-            line = (String) lines.getLast();
-            if (!checker.acceptString(line)) {
-                break;
-            } else {
-                elided++;
-                lines.removeLast();
-            }
-        }
-        if ((elided > 0) || (maxLines < 1)) { 
-            final int EOL_LEN = EOL.length();           
-            int totalLength = 0;
-            while (!lines.isEmpty()) { 
-                totalLength += EOL_LEN + ((String) lines.getFirst()).length();
-                lines.removeFirst();
-            }
-            if (stack.length() > totalLength) {
-                stack.setLength(totalLength);
-                if (elided > 0) {
-                    stack.append("    (... " + elided + " lines...)");
-                }
-            }
-        }
-    }
-
-
-    /** Dump message and stack to StringBuffer. */
-    public static StringBuffer stackToString(Throwable throwable, boolean skipMessage) {
-        if (null == throwable) {
-            return new StringBuffer();
-        }
-        StringWriter buf = new StringWriter();
-        PrintWriter writer = new PrintWriter(buf);
-        if (!skipMessage) {
-            writer.println(throwable.getMessage());
-        }
-        throwable.printStackTrace(writer);
-        try { buf.close(); } 
-        catch (IOException ioe) {} //  ignored
-        return buf.getBuffer(); 
-    }
-
-    
-    /** @return Throwable input or tail of any wrapped exception chain */
-    public static Throwable unwrapException(Throwable t) {
-        Throwable current = t;
-        Throwable next = null;
-        while (current != null) {   
-            // Java 1.2 exceptions that carry exceptions
-            if (current instanceof InvocationTargetException) {
-                next = ((InvocationTargetException) current).getTargetException();
-            } else if (current instanceof ClassNotFoundException) {
-                next = ((ClassNotFoundException) current).getException();
-            } else if (current instanceof ExceptionInInitializerError) {
-                next = ((ExceptionInInitializerError) current).getException();
-            } else if (current instanceof PrivilegedActionException) {
-                next = ((PrivilegedActionException) current).getException();
-            } else if (current instanceof SQLException) {
-                next = ((SQLException) current).getNextException();
-            }
-            // ...getException():
-            // javax.naming.event.NamingExceptionEvent
-            // javax.naming.ldap.UnsolicitedNotification
-            // javax.xml.parsers.FactoryConfigurationError
-            // javax.xml.transform.TransformerFactoryConfigurationError
-            // javax.xml.transform.TransformerException
-            // org.xml.sax.SAXException
-            // 1.4: Throwable.getCause
-            // java.util.logging.LogRecord.getThrown()
-            if (null == next) {
-                break;
-            } else {
-                current = next;
-                next = null;
-            }
-        }
-        return current;
-    }
+                                       }
+                               }
+                       }
+                       if (!found) {
+                               result[resultIndex++] = args[j];
+                       }
+               }
+
+               // unset any not found
+               for (int i = 0; i < options.length; i++) {
+                       if (!foundSet.get(i)) {
+                               options[i][0] = null;
+                       }
+               }
+               // fixup remainder
+               if (resultIndex < args.length) {
+                       String[] temp = new String[resultIndex];
+                       System.arraycopy(result, 0, temp, 0, resultIndex);
+                       args = temp;
+               }
+
+               return args;
+       }
+
+       //    
+       // /**
+       // * Extract options and arguments to input parameter list, returning remainder.
+       // * @param args the String[] input options
+       // * @param validOptions the String[] options to find in the input args - not null
+       // * @param optionArgs the int[] number of arguments for each option in validOptions
+       // * (if null, then no arguments for any option)
+       // * @param extracted the List for the matched options
+       // * @return String[] of args remaining after extracting options to extracted
+       // */
+       // public static String[] extractOptions(String[] args, String[] validOptions,
+       // int[] optionArgs, List extracted) {
+       // if (LangUtil.isEmpty(args)
+       // || LangUtil.isEmpty(validOptions) ) {
+       // return args;
+       // }
+       // if (null != optionArgs) {
+       // if (optionArgs.length != validOptions.length) {
+       // throw new IllegalArgumentException("args must match options");
+       // }
+       // }
+       // String[] result = new String[args.length];
+       // int resultIndex = 0;
+       // for (int j = 0; j < args.length; j++) {
+       // boolean found = false;
+       // for (int i = 0; !found && (i < validOptions.length); i++) {
+       // String sought = validOptions[i];
+       // int doMore = (null == optionArgs ? 0 : optionArgs[i]);
+       // if (LangUtil.isEmpty(sought)) {
+       // continue;
+       // }
+       // found = sought.equals(args[j]);
+       // if (found) {
+       // if (null != extracted) {
+       // extracted.add(sought);
+       // }
+       // if (0 < doMore) {
+       // final int MAX = j + doMore;
+       // if (MAX >= args.length) {
+       // String s = "expecting " + doMore + " args after ";
+       // throw new IllegalArgumentException(s + args[j]);
+       // }
+       // if (null != extracted) {
+       // while (j < MAX) {
+       // extracted.add(args[++j]);
+       // }
+       // } else {
+       // j = MAX;
+       // }
+       // }
+       // break;
+       // }
+       // }
+       // if (!found) {
+       // result[resultIndex++] = args[j];
+       // }
+       // }
+       // if (resultIndex < args.length) {
+       // String[] temp = new String[resultIndex];
+       // System.arraycopy(result, 0, temp, 0, resultIndex);
+       // args = temp;
+       // }
+       // return args;
+       // }
+
+       // /** @return String[] of entries in validOptions found in args */
+       // public static String[] selectOptions(String[] args, String[] validOptions) {
+       // if (LangUtil.isEmpty(args) || LangUtil.isEmpty(validOptions)) {
+       // return new String[0];
+       // }
+       // ArrayList result = new ArrayList();
+       // for (int i = 0; i < validOptions.length; i++) {
+       // String sought = validOptions[i];
+       // if (LangUtil.isEmpty(sought)) {
+       // continue;
+       // }
+       // for (int j = 0; j < args.length; j++) {
+       // if (sought.equals(args[j])) {
+       // result.add(sought);
+       // break;
+       // }
+       // }
+       // }
+       // return (String[]) result.toArray(new String[0]);
+       // }
+
+       // /** @return String[] of entries in validOptions found in args */
+       // public static String[] selectOptions(List args, String[] validOptions) {
+       // if (LangUtil.isEmpty(args) || LangUtil.isEmpty(validOptions)) {
+       // return new String[0];
+       // }
+       // ArrayList result = new ArrayList();
+       // for (int i = 0; i < validOptions.length; i++) {
+       // String sought = validOptions[i];
+       // if (LangUtil.isEmpty(sought)) {
+       // continue;
+       // }
+       // for (Iterator iter = args.iterator(); iter.hasNext();) {
+       // String arg = (String) iter.next();
+       // if (sought.equals(arg)) {
+       // result.add(sought);
+       // break;
+       // }
+       // }
+       // }
+       // return (String[]) result.toArray(new String[0]);
+       // }
+
+       // /**
+       // * Generate variants of String[] options by creating an extra set for
+       // * each option that ends with "-". If none end with "-", then an
+       // * array equal to <code>new String[][] { options }</code> is returned;
+       // * if one ends with "-", then two sets are returned,
+       // * three causes eight sets, etc.
+       // * @return String[][] with each option set.
+       // * @throws IllegalArgumentException if any option is null or empty.
+       // */
+       // public static String[][] optionVariants(String[] options) {
+       // if ((null == options) || (0 == options.length)) {
+       // return new String[][] { new String[0]};
+       // }
+       // // be nice, don't stomp input
+       // String[] temp = new String[options.length];
+       // System.arraycopy(options, 0, temp, 0, temp.length);
+       // options = temp;
+       // boolean[] dup = new boolean[options.length];
+       // int numDups = 0;
+       //        
+       // for (int i = 0; i < options.length; i++) {
+       // String option = options[i];
+       // if (LangUtil.isEmpty(option)) {
+       // throw new IllegalArgumentException("empty option at " + i);
+       // }
+       // if (option.endsWith("-")) {
+       // options[i] = option.substring(0, option.length()-1);
+       // dup[i] = true;
+       // numDups++;
+       // }
+       // }
+       // final String[] NONE = new String[0];
+       // final int variants = exp(2, numDups);
+       // final String[][] result = new String[variants][];
+       // // variant is a bitmap wrt doing extra value when dup[k]=true
+       // for (int variant = 0; variant < variants; variant++) {
+       // ArrayList next = new ArrayList();
+       // int nextOption = 0;
+       // for (int k = 0; k < options.length; k++) {
+       // if (!dup[k] || (0 != (variant & (1 << (nextOption++))))) {
+       // next.add(options[k]);
+       // }
+       // }
+       // result[variant] = (String[]) next.toArray(NONE);
+       // }
+       // return result;
+       // }
+       //    
+       // private static int exp(int base, int power) { // not in Math?
+       // if (0 > power) {
+       // throw new IllegalArgumentException("negative power: " + power);
+       // }
+       // int result = 1;
+       // while (0 < power--) {
+       // result *= base;
+       // }
+       // return result;
+       // }
+
+       // /**
+       // * Make a copy of the array.
+       // * @return an array with the same component type as source
+       // * containing same elements, even if null.
+       // * @throws IllegalArgumentException if source is null
+       // */
+       // public static final Object[] copy(Object[] source) {
+       // LangUtil.throwIaxIfNull(source, "source");
+       // final Class c = source.getClass().getComponentType();
+       // Object[] result = (Object[]) Array.newInstance(c, source.length);
+       // System.arraycopy(source, 0, result, 0, result.length);
+       // return result;
+       // }
+
+       /**
+        * Convert arrays safely. The number of elements in the result will be 1 smaller for each element that is null or not
+        * assignable. This will use sink if it has exactly the right size. The result will always have the same component type as sink.
+        * 
+        * @return an array with the same component type as sink containing any assignable elements in source (in the same order).
+        * @throws IllegalArgumentException if either is null
+        */
+       public static Object[] safeCopy(Object[] source, Object[] sink) {
+               final Class sinkType = (null == sink ? Object.class : sink.getClass().getComponentType());
+               final int sourceLength = (null == source ? 0 : source.length);
+               final int sinkLength = (null == sink ? 0 : sink.length);
+
+               final int resultSize;
+               ArrayList result = null;
+               if (0 == sourceLength) {
+                       resultSize = 0;
+               } else {
+                       result = new ArrayList(sourceLength);
+                       for (int i = 0; i < sourceLength; i++) {
+                               if ((null != source[i]) && (sinkType.isAssignableFrom(source[i].getClass()))) {
+                                       result.add(source[i]);
+                               }
+                       }
+                       resultSize = result.size();
+               }
+               if (resultSize != sinkLength) {
+                       sink = (Object[]) Array.newInstance(sinkType, result.size());
+               }
+               if (0 < resultSize) {
+                       sink = result.toArray(sink);
+               }
+               return sink;
+       }
+
+       /**
+        * @return a String with the unqualified class name of the class (or "null")
+        */
+       public static String unqualifiedClassName(Class c) {
+               if (null == c) {
+                       return "null";
+               }
+               String name = c.getName();
+               int loc = name.lastIndexOf(".");
+               if (-1 != loc) {
+                       name = name.substring(1 + loc);
+               }
+               return name;
+       }
+
+       /**
+        * @return a String with the unqualified class name of the object (or "null")
+        */
+       public static String unqualifiedClassName(Object o) {
+               return LangUtil.unqualifiedClassName(null == o ? null : o.getClass());
+       }
+
+       /** inefficient way to replace all instances of sought with replace */
+       public static String replace(String in, String sought, String replace) {
+               if (LangUtil.isEmpty(in) || LangUtil.isEmpty(sought)) {
+                       return in;
+               }
+               StringBuffer result = new StringBuffer();
+               final int len = sought.length();
+               int start = 0;
+               int loc;
+               while (-1 != (loc = in.indexOf(sought, start))) {
+                       result.append(in.substring(start, loc));
+                       if (!LangUtil.isEmpty(replace)) {
+                               result.append(replace);
+                       }
+                       start = loc + len;
+               }
+               result.append(in.substring(start));
+               return result.toString();
+       }
+
+       /** render i right-justified with a given width less than about 40 */
+       public static String toSizedString(long i, int width) {
+               String result = "" + i;
+               int size = result.length();
+               if (width > size) {
+                       final String pad = "                                              ";
+                       final int padLength = pad.length();
+                       if (width > padLength) {
+                               width = padLength;
+                       }
+                       int topad = width - size;
+                       result = pad.substring(0, topad) + result;
+               }
+               return result;
+       }
+
+       // /** clip StringBuffer to maximum number of lines */
+       // static String clipBuffer(StringBuffer buffer, int maxLines) {
+       // if ((null == buffer) || (1 > buffer.length())) return "";
+       // StringBuffer result = new StringBuffer();
+       // int j = 0;
+       // final int MAX = maxLines;
+       // final int N = buffer.length();
+       // for (int i = 0, srcBegin = 0; i < MAX; srcBegin += j) {
+       // // todo: replace with String variant if/since getting char?
+       // char[] chars = new char[128];
+       // int srcEnd = srcBegin+chars.length;
+       // if (srcEnd >= N) {
+       // srcEnd = N-1;
+       // }
+       // if (srcBegin == srcEnd) break;
+       // //log("srcBegin:" + srcBegin + ":srcEnd:" + srcEnd);
+       // buffer.getChars(srcBegin, srcEnd, chars, 0);
+       // for (j = 0; j < srcEnd-srcBegin/*chars.length*/; j++) {
+       // char c = chars[j];
+       // if (c == '\n') {
+       // i++;
+       // j++;
+       // break;
+       // }
+       // }
+       // try { result.append(chars, 0, j); }
+       // catch (Throwable t) { }
+       // }
+       // return result.toString();
+       // }
+
+       /**
+        * @return "({UnqualifiedExceptionClass}) {message}"
+        */
+       public static String renderExceptionShort(Throwable e) {
+               if (null == e)
+                       return "(Throwable) null";
+               return "(" + LangUtil.unqualifiedClassName(e) + ") " + e.getMessage();
+       }
+
+       /**
+        * Renders exception <code>t</code> after unwrapping and eliding any test packages.
+        * 
+        * @param t <code>Throwable</code> to print.
+        * @see #maxStackTrace
+        */
+       public static String renderException(Throwable t) {
+               return renderException(t, true);
+       }
+
+       /**
+        * Renders exception <code>t</code>, unwrapping, optionally eliding and limiting total number of lines.
+        * 
+        * @param t <code>Throwable</code> to print.
+        * @param elide true to limit to 100 lines and elide test packages
+        * @see StringChecker#TEST_PACKAGES
+        */
+       public static String renderException(Throwable t, boolean elide) {
+               if (null == t)
+                       return "null throwable";
+               t = unwrapException(t);
+               StringBuffer stack = stackToString(t, false);
+               if (elide) {
+                       elideEndingLines(StringChecker.TEST_PACKAGES, stack, 100);
+               }
+               return stack.toString();
+       }
+
+       /**
+        * Trim ending lines from a StringBuffer, clipping to maxLines and further removing any number of trailing lines accepted by
+        * checker.
+        * 
+        * @param checker returns true if trailing line should be elided.
+        * @param stack StringBuffer with lines to elide
+        * @param maxLines int for maximum number of resulting lines
+        */
+       static void elideEndingLines(StringChecker checker, StringBuffer stack, int maxLines) {
+               if (null == checker || (null == stack) || (0 == stack.length())) {
+                       return;
+               }
+               final LinkedList lines = new LinkedList();
+               StringTokenizer st = new StringTokenizer(stack.toString(), "\n\r");
+               while (st.hasMoreTokens() && (0 < --maxLines)) {
+                       lines.add(st.nextToken());
+               }
+               st = null;
+
+               String line;
+               int elided = 0;
+               while (!lines.isEmpty()) {
+                       line = (String) lines.getLast();
+                       if (!checker.acceptString(line)) {
+                               break;
+                       } else {
+                               elided++;
+                               lines.removeLast();
+                       }
+               }
+               if ((elided > 0) || (maxLines < 1)) {
+                       final int EOL_LEN = EOL.length();
+                       int totalLength = 0;
+                       while (!lines.isEmpty()) {
+                               totalLength += EOL_LEN + ((String) lines.getFirst()).length();
+                               lines.removeFirst();
+                       }
+                       if (stack.length() > totalLength) {
+                               stack.setLength(totalLength);
+                               if (elided > 0) {
+                                       stack.append("    (... " + elided + " lines...)");
+                               }
+                       }
+               }
+       }
+
+       /** Dump message and stack to StringBuffer. */
+       public static StringBuffer stackToString(Throwable throwable, boolean skipMessage) {
+               if (null == throwable) {
+                       return new StringBuffer();
+               }
+               StringWriter buf = new StringWriter();
+               PrintWriter writer = new PrintWriter(buf);
+               if (!skipMessage) {
+                       writer.println(throwable.getMessage());
+               }
+               throwable.printStackTrace(writer);
+               try {
+                       buf.close();
+               } catch (IOException ioe) {
+               } // ignored
+               return buf.getBuffer();
+       }
+
+       /** @return Throwable input or tail of any wrapped exception chain */
+       public static Throwable unwrapException(Throwable t) {
+               Throwable current = t;
+               Throwable next = null;
+               while (current != null) {
+                       // Java 1.2 exceptions that carry exceptions
+                       if (current instanceof InvocationTargetException) {
+                               next = ((InvocationTargetException) current).getTargetException();
+                       } else if (current instanceof ClassNotFoundException) {
+                               next = ((ClassNotFoundException) current).getException();
+                       } else if (current instanceof ExceptionInInitializerError) {
+                               next = ((ExceptionInInitializerError) current).getException();
+                       } else if (current instanceof PrivilegedActionException) {
+                               next = ((PrivilegedActionException) current).getException();
+                       } else if (current instanceof SQLException) {
+                               next = ((SQLException) current).getNextException();
+                       }
+                       // ...getException():
+                       // javax.naming.event.NamingExceptionEvent
+                       // javax.naming.ldap.UnsolicitedNotification
+                       // javax.xml.parsers.FactoryConfigurationError
+                       // javax.xml.transform.TransformerFactoryConfigurationError
+                       // javax.xml.transform.TransformerException
+                       // org.xml.sax.SAXException
+                       // 1.4: Throwable.getCause
+                       // java.util.logging.LogRecord.getThrown()
+                       if (null == next) {
+                               break;
+                       } else {
+                               current = next;
+                               next = null;
+                       }
+               }
+               return current;
+       }
 
        /**
-     * Replacement for Arrays.asList(..) which gacks on null
-     * and returns a List in which remove is an unsupported operation.
-     * @param array the Object[] to convert (may be null)
-     * @return the List corresponding to array (never null)
-     */
-    public static List arrayAsList(Object[] array) {
-        if ((null == array) || (1 > array.length)) {
-            return Collections.EMPTY_LIST;
-        }
-        ArrayList list = new ArrayList();
-        list.addAll(Arrays.asList(array));
-        return list;
-    }
-
-       
-
-    
-    /** check if input contains any packages to elide. */
-    public static class StringChecker {
-        static StringChecker TEST_PACKAGES = new StringChecker(new String[] 
-            { "org.aspectj.testing",
-              "org.eclipse.jdt.internal.junit",
-              "junit.framework.",
-              "org.apache.tools.ant.taskdefs.optional.junit.JUnitTestRunner"
-            }); 
-            
-        String[] infixes;
-
-        /** @param infixes adopted */
-        StringChecker(String[] infixes) {
-            this.infixes = infixes;
-        }
-
-        /** @return true if input contains infixes */
+        * Replacement for Arrays.asList(..) which gacks on null and returns a List in which remove is an unsupported operation.
+        * 
+        * @param array the Object[] to convert (may be null)
+        * @return the List corresponding to array (never null)
+        */
+       public static List arrayAsList(Object[] array) {
+               if ((null == array) || (1 > array.length)) {
+                       return Collections.EMPTY_LIST;
+               }
+               ArrayList list = new ArrayList();
+               list.addAll(Arrays.asList(array));
+               return list;
+       }
+
+       /** check if input contains any packages to elide. */
+       public static class StringChecker {
+               static StringChecker TEST_PACKAGES = new StringChecker(new String[] { "org.aspectj.testing",
+                               "org.eclipse.jdt.internal.junit", "junit.framework.",
+                               "org.apache.tools.ant.taskdefs.optional.junit.JUnitTestRunner" });
+
+               String[] infixes;
+
+               /** @param infixes adopted */
+               StringChecker(String[] infixes) {
+                       this.infixes = infixes;
+               }
+
+               /** @return true if input contains infixes */
                public boolean acceptString(String input) {
-            boolean result = false;
+                       boolean result = false;
                        if (!LangUtil.isEmpty(input)) {
-                for (int i = 0; !result && (i < infixes.length); i++) {
-                               result = (-1 != input.indexOf(infixes[i]));
-                       }
-            }
-            return result;
-               }
-    }
-
-    /**
-     * Gen classpath.     
-     * @param bootclasspath
-     * @param classpath
-     * @param classesDir
-     * @param outputJar
-     * @return String combining classpath elements
-     */
-    public static String makeClasspath( // XXX dumb implementation
-        String bootclasspath, 
-        String classpath, 
-        String classesDir, 
-        String outputJar) {
-        StringBuffer sb = new StringBuffer();
-        addIfNotEmpty(bootclasspath, sb, File.pathSeparator);
-        addIfNotEmpty(classpath, sb, File.pathSeparator);
-        if (!addIfNotEmpty(classesDir, sb, File.pathSeparator)) {
-            addIfNotEmpty(outputJar, sb, File.pathSeparator);
-        }
-        return sb.toString();
-    }
-
-    /**
-     * @param input ignored if null
-     * @param sink the StringBuffer to add input to - return false if null
-     * @param delimiter the String to append to input when added - ignored if empty
-     * @return true if input + delimiter added to sink
-     */
-    private static boolean addIfNotEmpty(String input, StringBuffer sink, String delimiter) {
-        if (LangUtil.isEmpty(input) || (null == sink)) {
-            return false;
-        }
-        sink.append(input);
-        if (!LangUtil.isEmpty(delimiter)) {
-            sink.append(delimiter);
-        }
-        return true;
-    }
-
-    
-    /**
-     * Create or initialize a process controller to run 
-     * a process in another VM asynchronously.
-     * @param controller the ProcessController to initialize, if not null
-     * @param classpath
-     * @param mainClass
-     * @param args
-     * @return initialized ProcessController
-     */
-    public static ProcessController makeProcess(
-        ProcessController controller,
-        String classpath, 
-        String mainClass, 
-        String[] args) {
-        File java = LangUtil.getJavaExecutable();
-        ArrayList cmd = new ArrayList();
-        cmd.add(java.getAbsolutePath());
-        cmd.add("-classpath");
-        cmd.add(classpath);
-        cmd.add(mainClass);
-        if (!LangUtil.isEmpty(args)) {
-            cmd.addAll(Arrays.asList(args));
-        }
-        String[] command = (String[]) cmd.toArray(new String[0]);
-        if (null == controller) {
-            controller = new ProcessController();
-        }
-        controller.init(command, mainClass);
-        return controller;
-    }
-    
-//    /**
-//     * Create a process to run asynchronously.
-//     * @param controller if not null, initialize this one
-//     * @param command the String[] command to run
-//     * @param controller the ProcessControl for streams and results
-//     */
-//    public static ProcessController makeProcess( // not needed?
-//        ProcessController controller, 
-//        String[] command, 
-//        String label) {
-//        if (null == controller) {
-//            controller = new ProcessController();
-//        }
-//        controller.init(command, label);
-//        return controller;
-//    }
-    
-    /**
-     * Find java executable File path from java.home system property.
-     * @return File associated with the java command, or null if not found.
-     */
-    public static File getJavaExecutable() { 
-        String javaHome = null;
-        File result = null;
-        //java.home
-        // java.class.path
-        // java.ext.dirs
-        try {
-            javaHome = System.getProperty("java.home");
-        } catch (Throwable t) {
-            // ignore
-        }
-        if (null != javaHome) {
-            File binDir = new File(javaHome, "bin");
-            if (binDir.isDirectory() && binDir.canRead()) {
-                String[] execs = new String[] { "java", "java.exe" };
-                for (int i = 0; i < execs.length; i++) {
-                    result = new File(binDir, execs[i]);
-                    if (result.canRead()) {
-                        break;
-                    }
-                }
-            }
-        }
-        return result;
-    } 
-    
-    /**
-     * Sleep for a particular period (in milliseconds).
-     * @param time the long time in milliseconds to sleep
-     * @return true if delay succeeded, false if interrupted 100 times
-     */
-    public static boolean sleep(long milliseconds) {
-        if (milliseconds == 0) {
-            return true;
-        } else if (milliseconds < 0) {
-            throw new IllegalArgumentException("negative: " + milliseconds);
-        }
-        return sleepUntil(milliseconds + System.currentTimeMillis());
-    }
-    
-    /**
-     * Sleep until a particular time.
-     * @param time the long time in milliseconds to sleep until
-     * @return true if delay succeeded, false if interrupted 100 times
-     */
-    public static boolean sleepUntil(long time) {
-        if (time == 0) {
-            return true;
-        } else if (time < 0) {
-            throw new IllegalArgumentException("negative: " + time);
-        }
-//        final Thread thread = Thread.currentThread();
-        long curTime = System.currentTimeMillis();
-        for (int i = 0; (i < 100) && (curTime < time); i++) {
-            try {
-                Thread.sleep(time-curTime);            
-            } catch (InterruptedException e) {
-                // ignore
-            }
-            curTime = System.currentTimeMillis();
-        }
-        return (curTime >= time);
-    }  
-      
-    /**
-     * Handle an external process asynchrously.
-     * <code>start()</code> launches a main thread to wait for the process 
-     * and pipes streams (in child threads) through to the corresponding
-     * streams (e.g., the process System.err to this System.err).
-     * This can complete normally, by exception, or on demand by a client.
-     * Clients can implement <code>doCompleting(..)</code> to get notice
-     * when the process completes.
-     * <p>The following sample code creates a process with a completion 
-     * callback starts it, and some time later retries the process.
-     * <pre>
-     * LangUtil.ProcessController controller 
-     *   = new LangUtil.ProcessController() {
-     *   protected void doCompleting(LangUtil.ProcessController.Thrown thrown, 
-     *                               int result) {
-     *      // signal result 
-     *   }
-     * };
-     * controller.init(new String[] { "java", "-version" }, "java version");
-     * controller.start();
-     * // some time later...
-     * // retry...
-     * if (!controller.completed()) {
-     *     controller.stop();
-     *     controller.reinit();
-     *     controller.start();
-     * }
-     * </pre>
-     * <u>warning</u>: Currently this does not close the input or output
-     * streams, since doing so prevents their use later.
-     */
-    public static class ProcessController {
-        /*
-         * XXX not verified thread-safe, but should be.  Known problems:
-         * - user stops (completed = true) then exception thrown
-         *   from destroying process (stop() expects !completed)
-         * ...
-         */
-        private String[] command;
-        private String[] envp;
-        private String label;
-        
-        private boolean init;
-        private boolean started;
-        private boolean completed;
-        /** if true, stopped by user when not completed */
-        private boolean userStopped;
-
-        private Process process;
-        private FileUtil.Pipe errStream;
-        private FileUtil.Pipe outStream;
-        private FileUtil.Pipe inStream;
-        private ByteArrayOutputStream errSnoop;
-        private ByteArrayOutputStream outSnoop;
-
-        private int result;
-        private Thrown thrown;     
-        
-        public ProcessController() {
-        }
-        
-        /**
-         * Permit re-running using the same command
-         * if this is not started or if completed.
-         * Can also call this when done with results to release 
-         * references associated with results
-         * (e.g., stack traces).
-         */
-        public final void reinit() {
-            if (!init) {
-                throw new IllegalStateException("must init(..) before reinit()");
-            }
-            if (started && !completed) {
-                throw new IllegalStateException("not completed - do stop()");
-            }
-            // init everything but command and label
-            started = false;
-            completed = false;
-            result = Integer.MIN_VALUE;
-            thrown = null;
-            process = null;
-            errStream = null;
-            outStream = null;
-            inStream = null;
-        }
-
-        public final void init(
-                String classpath, 
-                String mainClass, 
-                String[] args) {
-            init(LangUtil.getJavaExecutable(),
-                    classpath, mainClass, args);
-        }
-
-        public final void init(File java,
-                String classpath, 
-                String mainClass, 
-                String[] args) {
-            LangUtil.throwIaxIfNull(java, "java");
-            LangUtil.throwIaxIfNull(mainClass, "mainClass");
-            LangUtil.throwIaxIfNull(args, "args");
-            ArrayList cmd = new ArrayList();
-            cmd.add(java.getAbsolutePath());
-            cmd.add("-classpath");
-            cmd.add(classpath);
-            cmd.add(mainClass);
-            if (!LangUtil.isEmpty(args)) {
-                cmd.addAll(Arrays.asList(args));
-            }
-            init((String[]) cmd.toArray(new String[0]), mainClass);
-        }        
-        
-        public final void init(String[] command, String label) {
-            this.command = (String[]) LangUtil.safeCopy(command, new String[0]);
-            if (1 > this.command.length) {
-                throw new IllegalArgumentException("empty command");
-            }
-            this.label = LangUtil.isEmpty(label) ? command[0] : label;
-            this.init = true;
-            reinit();
-        }
-
-        public final void setEnvp(String[] envp) {
-            this.envp = (String[]) LangUtil.safeCopy(envp, new String[0]);
-            if (1 > this.envp.length) {
-                throw new IllegalArgumentException("empty envp");
-            }
-        }
-
-        public final void setErrSnoop(ByteArrayOutputStream snoop) {        
-            errSnoop = snoop;
-            if (null != errStream) {
-                errStream.setSnoop(errSnoop);
-            }
-        }
-        
-        public final void setOutSnoop(ByteArrayOutputStream snoop) {        
-            outSnoop = snoop;
-            if (null != outStream) {
-                outStream.setSnoop(outSnoop);
-            }
-        }
-
-        /** 
-         * Start running the process and pipes asynchronously.
-         * @return Thread started or null if unable to start thread
-         *         (results available via <code>getThrown()</code>, etc.) 
-         */
-        public final Thread start() {
-            if (!init) { 
-                throw new IllegalStateException("not initialized");
-            }
-            synchronized (this) {
-                if (started) {
-                    throw new IllegalStateException("already started");
-                }
-                started = true;
-            }
-            try {
-                process = Runtime.getRuntime().exec(command);
-            } catch (IOException e) {
-                stop(e, Integer.MIN_VALUE);
-                return null;
-            }
-            errStream = new FileUtil.Pipe(process.getErrorStream(), System.err);
-            if (null != errSnoop) {
-                errStream.setSnoop(errSnoop);
-            }
-            outStream = new FileUtil.Pipe(process.getInputStream(), System.out);
-            if (null != outSnoop) {
-                outStream.setSnoop(outSnoop);
-            }
-            inStream = new FileUtil.Pipe(System.in, process.getOutputStream());
-            // start 4 threads, process & pipes for in, err, out
-            Runnable processRunner = new Runnable() {
-                public void run() {
-                    Throwable thrown = null;
-                    int result = Integer.MIN_VALUE;
-                    try {
-                        // pipe threads are children
-                        new Thread(errStream).start();
-                        new Thread(outStream).start();
-                        new Thread(inStream).start();
-                        process.waitFor();
-                        result = process.exitValue();
-                    } catch (Throwable e) {
-                        thrown = e;
-                    } finally {
-                        stop(thrown, result);
-                    }
-                }
-            };
-            Thread result = new Thread(processRunner, label);
-            result.start();
-            return result;
-        }
-
-        /** 
-         * Destroy any process, stop any pipes.
-         * This waits for the pipes to clear (reading until
-         * no more input is available), but does not wait
-         * for the input stream for the pipe to close
-         * (i.e., not waiting for end-of-file on input stream). 
-         */
-        public final synchronized void stop() {
-            if (completed) {
-                return;
-            }
-            userStopped = true;
-            stop(null, Integer.MIN_VALUE);
-        }
-        
-        public final String[] getCommand() {
-            String[] toCopy = command;
-            if (LangUtil.isEmpty(toCopy)) {
-                return new String[0];
-            }
-            String[] result = new String[toCopy.length];
-            System.arraycopy(toCopy, 0, result, 0, result.length);
-            return result;
-        }
-        
-        public final boolean completed() {
-            return completed;
-        }
-        
-        public final boolean started() {
-            return started;
-        }
-        
-        public final boolean userStopped() {
-            return userStopped;
-        }
-        
-        /**
-         * Get any Throwable thrown.
-         * Note that the process can complete normally (with a valid return
-         * value), at the same time the pipes throw exceptions,
-         * and that this may return some exceptions even if the process
-         * is not complete.
-         * @return null if not complete or 
-         *         Thrown containing exceptions thrown by the process and streams.
-         */
-        public final Thrown getThrown() { // cache this
-            return makeThrown(null);
-        }
-        
-        public final int getResult() {
-            return result;
-        }
-        
-        /** 
-         * Subclasses implement this to get synchronous notice of completion.
-         * All pipes and processes should be complete at this time. 
-         * To get the exceptions thrown for the pipes, use
-         * <code>getThrown()</code>.
-         * If there is an exception, the process completed abruptly
-         * (including side-effects of the user halting the process).
-         * If <code>userStopped()</code> is true, then some client asked
-         * that the process be destroyed using <code>stop()</code>.
-         * Otherwise, the result code should be the result value
-         * returned by the process.
-         * @param thrown same as <code>getThrown().fromProcess</code>.
-         * @param result same as <code>getResult()</code>
-         * @see getThrown()
-         * @see getResult()
-         * @see stop()
-         */
-        protected void doCompleting(Thrown thrown, int result) {
-        }
-
-        /**
-         * Handle termination (on-demand, abrupt, or normal)
-         * by destroying and/or halting process and pipes. 
-         * @param thrown ignored if null
-         * @param result ignored if Integer.MIN_VALUE
-         */
-        private final synchronized void stop(Throwable thrown, int result) {
-            if (completed) {
-                throw new IllegalStateException("already completed");
-            } else if (null != this.thrown) {
-                throw new IllegalStateException("already set thrown: " + thrown);
-            }
-            // assert null == this.thrown
-            this.thrown = makeThrown(thrown);
-            if (null != process) {
-                process.destroy();        
-            }
-            if (null != inStream) {
-                inStream.halt(false, true); // this will block if waiting
-                inStream = null;
-            }
-            if (null != outStream) {
-                outStream.halt(true, true);
-                outStream = null;
-            }
-            if (null != errStream) {
-                errStream.halt(true, true);
-                errStream = null;
-            }
-            if (Integer.MIN_VALUE != result) {
-                this.result = result;
-            }
-            completed = true;
-            doCompleting(this.thrown, result);
-        }
-        
-        /**
-         * Create snapshot of Throwable's thrown.
-         * @param thrown ignored if null or if this.thrown is not null
-         */
-        private final synchronized Thrown makeThrown(Throwable processThrown) {
-            if (null != thrown) {
-                return thrown;
-            }
-            return new Thrown(
-                processThrown,
-                (null == outStream ? null : outStream.getThrown()),
-                (null == errStream ? null : errStream.getThrown()),
-                (null == inStream ? null : inStream.getThrown())
-            );
-        }
-        
-        public static class Thrown {
-            public final Throwable fromProcess; 
-            public final Throwable fromErrPipe; 
-            public final Throwable fromOutPipe; 
-            public final Throwable fromInPipe;
-            /** true only if some Throwable is not null */
-            public final boolean thrown;
-            private Thrown(
-                Throwable fromProcess,
-                Throwable fromOutPipe, 
-                Throwable fromErrPipe, 
-                Throwable fromInPipe) {
-                this.fromProcess = fromProcess;
-                this.fromErrPipe = fromErrPipe;
-                this.fromOutPipe = fromOutPipe;
-                this.fromInPipe = fromInPipe;
-                thrown = ((null != fromProcess)
-                            || (null != fromInPipe)
-                            || (null != fromOutPipe)
-                            || (null != fromErrPipe));
-            }
-            
-            public String toString() {
-                StringBuffer sb = new StringBuffer();
-                append(sb, fromProcess, "process");
-                append(sb, fromOutPipe, " stdout");
-                append(sb, fromErrPipe, " stderr");
-                append(sb, fromInPipe,  "  stdin");
-                if (0 == sb.length()) {
-                    return "Thrown (none)";
-                } else {
-                    return sb.toString();
-                }
-            }
-            private void append(StringBuffer sb, Throwable thrown, String label) {
-                if (null != thrown) {
-                    sb.append("from " + label + ": ");
-                    sb.append(LangUtil.renderExceptionShort(thrown));
-                    sb.append(LangUtil.EOL);
-                }
-            }
-        } // class Thrown
-    }
+                               for (int i = 0; !result && (i < infixes.length); i++) {
+                                       result = (-1 != input.indexOf(infixes[i]));
+                               }
+                       }
+                       return result;
+               }
+       }
 
-}
+       /**
+        * Gen classpath.
+        * 
+        * @param bootclasspath
+        * @param classpath
+        * @param classesDir
+        * @param outputJar
+        * @return String combining classpath elements
+        */
+       public static String makeClasspath( // XXX dumb implementation
+                       String bootclasspath, String classpath, String classesDir, String outputJar) {
+               StringBuffer sb = new StringBuffer();
+               addIfNotEmpty(bootclasspath, sb, File.pathSeparator);
+               addIfNotEmpty(classpath, sb, File.pathSeparator);
+               if (!addIfNotEmpty(classesDir, sb, File.pathSeparator)) {
+                       addIfNotEmpty(outputJar, sb, File.pathSeparator);
+               }
+               return sb.toString();
+       }
+
+       /**
+        * @param input ignored if null
+        * @param sink the StringBuffer to add input to - return false if null
+        * @param delimiter the String to append to input when added - ignored if empty
+        * @return true if input + delimiter added to sink
+        */
+       private static boolean addIfNotEmpty(String input, StringBuffer sink, String delimiter) {
+               if (LangUtil.isEmpty(input) || (null == sink)) {
+                       return false;
+               }
+               sink.append(input);
+               if (!LangUtil.isEmpty(delimiter)) {
+                       sink.append(delimiter);
+               }
+               return true;
+       }
+
+       /**
+        * Create or initialize a process controller to run a process in another VM asynchronously.
+        * 
+        * @param controller the ProcessController to initialize, if not null
+        * @param classpath
+        * @param mainClass
+        * @param args
+        * @return initialized ProcessController
+        */
+       public static ProcessController makeProcess(ProcessController controller, String classpath, String mainClass, String[] args) {
+               File java = LangUtil.getJavaExecutable();
+               ArrayList cmd = new ArrayList();
+               cmd.add(java.getAbsolutePath());
+               cmd.add("-classpath");
+               cmd.add(classpath);
+               cmd.add(mainClass);
+               if (!LangUtil.isEmpty(args)) {
+                       cmd.addAll(Arrays.asList(args));
+               }
+               String[] command = (String[]) cmd.toArray(new String[0]);
+               if (null == controller) {
+                       controller = new ProcessController();
+               }
+               controller.init(command, mainClass);
+               return controller;
+       }
+
+       // /**
+       // * Create a process to run asynchronously.
+       // * @param controller if not null, initialize this one
+       // * @param command the String[] command to run
+       // * @param controller the ProcessControl for streams and results
+       // */
+       // public static ProcessController makeProcess( // not needed?
+       // ProcessController controller,
+       // String[] command,
+       // String label) {
+       // if (null == controller) {
+       // controller = new ProcessController();
+       // }
+       // controller.init(command, label);
+       // return controller;
+       // }
+
+       /**
+        * Find java executable File path from java.home system property.
+        * 
+        * @return File associated with the java command, or null if not found.
+        */
+       public static File getJavaExecutable() {
+               String javaHome = null;
+               File result = null;
+               // java.home
+               // java.class.path
+               // java.ext.dirs
+               try {
+                       javaHome = System.getProperty("java.home");
+               } catch (Throwable t) {
+                       // ignore
+               }
+               if (null != javaHome) {
+                       File binDir = new File(javaHome, "bin");
+                       if (binDir.isDirectory() && binDir.canRead()) {
+                               String[] execs = new String[] { "java", "java.exe" };
+                               for (int i = 0; i < execs.length; i++) {
+                                       result = new File(binDir, execs[i]);
+                                       if (result.canRead()) {
+                                               break;
+                                       }
+                               }
+                       }
+               }
+               return result;
+       }
 
+       /**
+        * Sleep for a particular period (in milliseconds).
+        * 
+        * @param time the long time in milliseconds to sleep
+        * @return true if delay succeeded, false if interrupted 100 times
+        */
+       public static boolean sleep(long milliseconds) {
+               if (milliseconds == 0) {
+                       return true;
+               } else if (milliseconds < 0) {
+                       throw new IllegalArgumentException("negative: " + milliseconds);
+               }
+               return sleepUntil(milliseconds + System.currentTimeMillis());
+       }
+
+       /**
+        * Sleep until a particular time.
+        * 
+        * @param time the long time in milliseconds to sleep until
+        * @return true if delay succeeded, false if interrupted 100 times
+        */
+       public static boolean sleepUntil(long time) {
+               if (time == 0) {
+                       return true;
+               } else if (time < 0) {
+                       throw new IllegalArgumentException("negative: " + time);
+               }
+               // final Thread thread = Thread.currentThread();
+               long curTime = System.currentTimeMillis();
+               for (int i = 0; (i < 100) && (curTime < time); i++) {
+                       try {
+                               Thread.sleep(time - curTime);
+                       } catch (InterruptedException e) {
+                               // ignore
+                       }
+                       curTime = System.currentTimeMillis();
+               }
+               return (curTime >= time);
+       }
+
+       /**
+        * Handle an external process asynchrously. <code>start()</code> launches a main thread to wait for the process and pipes
+        * streams (in child threads) through to the corresponding streams (e.g., the process System.err to this System.err). This can
+        * complete normally, by exception, or on demand by a client. Clients can implement <code>doCompleting(..)</code> to get notice
+        * when the process completes.
+        * <p>
+        * The following sample code creates a process with a completion callback starts it, and some time later retries the process.
+        * 
+        * <pre>
+        * LangUtil.ProcessController controller = new LangUtil.ProcessController() {
+        *      protected void doCompleting(LangUtil.ProcessController.Thrown thrown, int result) {
+        *              // signal result 
+        *      }
+        * };
+        * controller.init(new String[] { &quot;java&quot;, &quot;-version&quot; }, &quot;java version&quot;);
+        * controller.start();
+        * // some time later...
+        * // retry...
+        * if (!controller.completed()) {
+        *      controller.stop();
+        *      controller.reinit();
+        *      controller.start();
+        * }
+        * </pre>
+        * 
+        * <u>warning</u>: Currently this does not close the input or output streams, since doing so prevents their use later.
+        */
+       public static class ProcessController {
+               /*
+                * XXX not verified thread-safe, but should be. Known problems: - user stops (completed = true) then exception thrown from
+                * destroying process (stop() expects !completed) ...
+                */
+               private String[] command;
+               private String[] envp;
+               private String label;
+
+               private boolean init;
+               private boolean started;
+               private boolean completed;
+               /** if true, stopped by user when not completed */
+               private boolean userStopped;
+
+               private Process process;
+               private FileUtil.Pipe errStream;
+               private FileUtil.Pipe outStream;
+               private FileUtil.Pipe inStream;
+               private ByteArrayOutputStream errSnoop;
+               private ByteArrayOutputStream outSnoop;
+
+               private int result;
+               private Thrown thrown;
+
+               public ProcessController() {
+               }
+
+               /**
+                * Permit re-running using the same command if this is not started or if completed. Can also call this when done with
+                * results to release references associated with results (e.g., stack traces).
+                */
+               public final void reinit() {
+                       if (!init) {
+                               throw new IllegalStateException("must init(..) before reinit()");
+                       }
+                       if (started && !completed) {
+                               throw new IllegalStateException("not completed - do stop()");
+                       }
+                       // init everything but command and label
+                       started = false;
+                       completed = false;
+                       result = Integer.MIN_VALUE;
+                       thrown = null;
+                       process = null;
+                       errStream = null;
+                       outStream = null;
+                       inStream = null;
+               }
+
+               public final void init(String classpath, String mainClass, String[] args) {
+                       init(LangUtil.getJavaExecutable(), classpath, mainClass, args);
+               }
+
+               public final void init(File java, String classpath, String mainClass, String[] args) {
+                       LangUtil.throwIaxIfNull(java, "java");
+                       LangUtil.throwIaxIfNull(mainClass, "mainClass");
+                       LangUtil.throwIaxIfNull(args, "args");
+                       ArrayList cmd = new ArrayList();
+                       cmd.add(java.getAbsolutePath());
+                       cmd.add("-classpath");
+                       cmd.add(classpath);
+                       cmd.add(mainClass);
+                       if (!LangUtil.isEmpty(args)) {
+                               cmd.addAll(Arrays.asList(args));
+                       }
+                       init((String[]) cmd.toArray(new String[0]), mainClass);
+               }
+
+               public final void init(String[] command, String label) {
+                       this.command = (String[]) LangUtil.safeCopy(command, new String[0]);
+                       if (1 > this.command.length) {
+                               throw new IllegalArgumentException("empty command");
+                       }
+                       this.label = LangUtil.isEmpty(label) ? command[0] : label;
+                       this.init = true;
+                       reinit();
+               }
+
+               public final void setEnvp(String[] envp) {
+                       this.envp = (String[]) LangUtil.safeCopy(envp, new String[0]);
+                       if (1 > this.envp.length) {
+                               throw new IllegalArgumentException("empty envp");
+                       }
+               }
+
+               public final void setErrSnoop(ByteArrayOutputStream snoop) {
+                       errSnoop = snoop;
+                       if (null != errStream) {
+                               errStream.setSnoop(errSnoop);
+                       }
+               }
 
+               public final void setOutSnoop(ByteArrayOutputStream snoop) {
+                       outSnoop = snoop;
+                       if (null != outStream) {
+                               outStream.setSnoop(outSnoop);
+                       }
+               }
+
+               /**
+                * Start running the process and pipes asynchronously.
+                * 
+                * @return Thread started or null if unable to start thread (results available via <code>getThrown()</code>, etc.)
+                */
+               public final Thread start() {
+                       if (!init) {
+                               throw new IllegalStateException("not initialized");
+                       }
+                       synchronized (this) {
+                               if (started) {
+                                       throw new IllegalStateException("already started");
+                               }
+                               started = true;
+                       }
+                       try {
+                               process = Runtime.getRuntime().exec(command);
+                       } catch (IOException e) {
+                               stop(e, Integer.MIN_VALUE);
+                               return null;
+                       }
+                       errStream = new FileUtil.Pipe(process.getErrorStream(), System.err);
+                       if (null != errSnoop) {
+                               errStream.setSnoop(errSnoop);
+                       }
+                       outStream = new FileUtil.Pipe(process.getInputStream(), System.out);
+                       if (null != outSnoop) {
+                               outStream.setSnoop(outSnoop);
+                       }
+                       inStream = new FileUtil.Pipe(System.in, process.getOutputStream());
+                       // start 4 threads, process & pipes for in, err, out
+                       Runnable processRunner = new Runnable() {
+                               public void run() {
+                                       Throwable thrown = null;
+                                       int result = Integer.MIN_VALUE;
+                                       try {
+                                               // pipe threads are children
+                                               new Thread(errStream).start();
+                                               new Thread(outStream).start();
+                                               new Thread(inStream).start();
+                                               process.waitFor();
+                                               result = process.exitValue();
+                                       } catch (Throwable e) {
+                                               thrown = e;
+                                       } finally {
+                                               stop(thrown, result);
+                                       }
+                               }
+                       };
+                       Thread result = new Thread(processRunner, label);
+                       result.start();
+                       return result;
+               }
+
+               /**
+                * Destroy any process, stop any pipes. This waits for the pipes to clear (reading until no more input is available), but
+                * does not wait for the input stream for the pipe to close (i.e., not waiting for end-of-file on input stream).
+                */
+               public final synchronized void stop() {
+                       if (completed) {
+                               return;
+                       }
+                       userStopped = true;
+                       stop(null, Integer.MIN_VALUE);
+               }
+
+               public final String[] getCommand() {
+                       String[] toCopy = command;
+                       if (LangUtil.isEmpty(toCopy)) {
+                               return new String[0];
+                       }
+                       String[] result = new String[toCopy.length];
+                       System.arraycopy(toCopy, 0, result, 0, result.length);
+                       return result;
+               }
+
+               public final boolean completed() {
+                       return completed;
+               }
+
+               public final boolean started() {
+                       return started;
+               }
+
+               public final boolean userStopped() {
+                       return userStopped;
+               }
+
+               /**
+                * Get any Throwable thrown. Note that the process can complete normally (with a valid return value), at the same time the
+                * pipes throw exceptions, and that this may return some exceptions even if the process is not complete.
+                * 
+                * @return null if not complete or Thrown containing exceptions thrown by the process and streams.
+                */
+               public final Thrown getThrown() { // cache this
+                       return makeThrown(null);
+               }
+
+               public final int getResult() {
+                       return result;
+               }
+
+               /**
+                * Subclasses implement this to get synchronous notice of completion. All pipes and processes should be complete at this
+                * time. To get the exceptions thrown for the pipes, use <code>getThrown()</code>. If there is an exception, the process
+                * completed abruptly (including side-effects of the user halting the process). If <code>userStopped()</code> is true, then
+                * some client asked that the process be destroyed using <code>stop()</code>. Otherwise, the result code should be the
+                * result value returned by the process.
+                * 
+                * @param thrown same as <code>getThrown().fromProcess</code>.
+                * @param result same as <code>getResult()</code>
+                * @see getThrown()
+                * @see getResult()
+                * @see stop()
+                */
+               protected void doCompleting(Thrown thrown, int result) {
+               }
+
+               /**
+                * Handle termination (on-demand, abrupt, or normal) by destroying and/or halting process and pipes.
+                * 
+                * @param thrown ignored if null
+                * @param result ignored if Integer.MIN_VALUE
+                */
+               private final synchronized void stop(Throwable thrown, int result) {
+                       if (completed) {
+                               throw new IllegalStateException("already completed");
+                       } else if (null != this.thrown) {
+                               throw new IllegalStateException("already set thrown: " + thrown);
+                       }
+                       // assert null == this.thrown
+                       this.thrown = makeThrown(thrown);
+                       if (null != process) {
+                               process.destroy();
+                       }
+                       if (null != inStream) {
+                               inStream.halt(false, true); // this will block if waiting
+                               inStream = null;
+                       }
+                       if (null != outStream) {
+                               outStream.halt(true, true);
+                               outStream = null;
+                       }
+                       if (null != errStream) {
+                               errStream.halt(true, true);
+                               errStream = null;
+                       }
+                       if (Integer.MIN_VALUE != result) {
+                               this.result = result;
+                       }
+                       completed = true;
+                       doCompleting(this.thrown, result);
+               }
+
+               /**
+                * Create snapshot of Throwable's thrown.
+                * 
+                * @param thrown ignored if null or if this.thrown is not null
+                */
+               private final synchronized Thrown makeThrown(Throwable processThrown) {
+                       if (null != thrown) {
+                               return thrown;
+                       }
+                       return new Thrown(processThrown, (null == outStream ? null : outStream.getThrown()), (null == errStream ? null
+                                       : errStream.getThrown()), (null == inStream ? null : inStream.getThrown()));
+               }
+
+               public static class Thrown {
+                       public final Throwable fromProcess;
+                       public final Throwable fromErrPipe;
+                       public final Throwable fromOutPipe;
+                       public final Throwable fromInPipe;
+                       /** true only if some Throwable is not null */
+                       public final boolean thrown;
+
+                       private Thrown(Throwable fromProcess, Throwable fromOutPipe, Throwable fromErrPipe, Throwable fromInPipe) {
+                               this.fromProcess = fromProcess;
+                               this.fromErrPipe = fromErrPipe;
+                               this.fromOutPipe = fromOutPipe;
+                               this.fromInPipe = fromInPipe;
+                               thrown = ((null != fromProcess) || (null != fromInPipe) || (null != fromOutPipe) || (null != fromErrPipe));
+                       }
+
+                       public String toString() {
+                               StringBuffer sb = new StringBuffer();
+                               append(sb, fromProcess, "process");
+                               append(sb, fromOutPipe, " stdout");
+                               append(sb, fromErrPipe, " stderr");
+                               append(sb, fromInPipe, "  stdin");
+                               if (0 == sb.length()) {
+                                       return "Thrown (none)";
+                               } else {
+                                       return sb.toString();
+                               }
+                       }
+
+                       private void append(StringBuffer sb, Throwable thrown, String label) {
+                               if (null != thrown) {
+                                       sb.append("from " + label + ": ");
+                                       sb.append(LangUtil.renderExceptionShort(thrown));
+                                       sb.append(LangUtil.EOL);
+                               }
+                       }
+               } // class Thrown
+       }
+
+}