org.aspectj.util.FileUtil.java Source code

Java tutorial

Introduction

Here is the source code for org.aspectj.util.FileUtil.java

Source

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

package org.aspectj.util;

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.OutputStream;
import java.io.PrintStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
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;

/**
 * @author Andy Clement
 * @author Kris De Volder
 */
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<String> 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<String> iter = SOURCE_SUFFIXES.iterator(); iter.hasNext();) {
            String suffix = 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 (null != files[i]) {
                result[i] = files[i].getPath();
            }
        }
        return result;
    }

    /** @return array same length as input, with String paths */
    public static String[] getPaths(List<File> 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 = 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 (!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);
            }
            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;
    }

    public static File getBestFile(String[] paths, boolean mustBeJar) {
        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()) {
                    if (mustBeJar && !f.isDirectory()) {
                        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();
        if (fromFiles == null) {
            return 0;
        }
        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);
                    String[] fileContent = file.list();
                    if (deleteEmptyDirs && fileContent != null && 0 == fileContent.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<String> result = new ArrayList<String>();
        if ((null != srcDir) && srcDir.canRead()) {
            listFiles(srcDir, null, result);
        }
        return 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<File> result = new ArrayList<File>();
        if ((null != srcDir) && srcDir.canRead()) {
            listFiles(srcDir, result, fileFilter);
        }
        return result.toArray(new File[result.size()]);
    }

    /**
     * Recursively list .class files in specified directory
     *
     * @return List of File objects
     */
    public static List<File> listClassFiles(File dir) {
        ArrayList<File> result = new ArrayList<File>();
        if ((null != dir) && dir.canRead()) {
            listClassFiles(dir, result);
        }
        return result;
    }

    /**
     * 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<File> list = new ArrayList<File>();
            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 = 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 */
    @SuppressWarnings("deprecation")
    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)
     */
    @SuppressWarnings("deprecation")
    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);
        }
        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
            }
        }
    }

    /**
     * Reads a boolean array with our encoding
     */
    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();
        }
        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]);
        }
    }

    /**
     * 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();
        }
        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]);
        }
    }

    /**
     * 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();
        }
        return ret;
    }

    /**
     * Writes an int array with our encoding
     */
    public static void writeStringArray(String[] a, DataOutputStream s) throws IOException {
        if (a == null) {
            s.writeInt(0);
            return;
        }
        int len = a.length;
        s.writeInt(len);
        for (int i = 0; i < len; i++) {
            s.writeUTF(a[i]);
        }
    }

    /**
     * Returns the contents of this file as a String
     */
    public static String readAsString(File file) throws IOException {
        BufferedReader r = new BufferedReader(new FileReader(file));
        StringBuffer b = new StringBuffer();
        while (true) {
            int ch = r.read();
            if (ch == -1) {
                break;
            }
            b.append((char) ch);
        }
        r.close();
        return b.toString();
    }

    // /**
    // * Returns the contents of this stream as a String
    // */
    // public static String readAsString(InputStream in) throws IOException {
    // BufferedReader r = new BufferedReader(new InputStreamReader(in));
    // StringBuffer b = new StringBuffer();
    // while (true) {
    // int ch = r.read();
    // if (ch == -1)
    // break;
    // b.append((char) ch);
    // }
    // in.close();
    // r.close();
    // return b.toString();
    // }

    /**
     * Returns the contents of this file as a byte[]
     */
    public static byte[] readAsByteArray(File file) throws IOException {
        FileInputStream in = new FileInputStream(file);
        byte[] ret = FileUtil.readAsByteArray(in);
        in.close();
        return ret;
    }

    /**
     * Reads this input stream and returns contents as a byte[]
     */
    public static byte[] readAsByteArray(InputStream inStream) throws IOException {
        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;
            }
            readSoFar += nRead;
            if (readSoFar == size) {
                int newSize = size * 2;
                byte[] newBa = new byte[newSize];
                System.arraycopy(ba, 0, newBa, 0, size);
                ba = newBa;
                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));
        }
        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);
            } finally {
                // ??? 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;
    // 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());
    // outFile.getParentFile().mkdirs();
    // FileOutputStream os = new FileOutputStream(outFile);
    // os.write(in);
    // os.close();
    // zs.closeEntry();
    // }
    // zs.close();
    // }

    /**
     * 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)
     */
    // OPTIMIZE only used by tests? move it out
    public static List<String> lineSeek(String sought, List<String> sources, boolean listAll,
            PrintStream errorSink) {
        if (LangUtil.isEmpty(sought) || LangUtil.isEmpty(sources)) {
            return Collections.emptyList();
        }
        ArrayList<String> result = new ArrayList<String>();
        for (Iterator<String> iter = sources.iterator(); iter.hasNext();) {
            String path = 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<String> 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();
        }
        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 listClassFiles(final File baseDir, ArrayList<File> result) {
        File[] files = baseDir.listFiles();
        for (int i = 0; i < files.length; i++) {
            File f = files[i];
            if (f.isDirectory()) {
                listClassFiles(f, result);
            } else {
                if (f.getName().endsWith(".class")) {
                    result.add(f);
                }
            }
        }
    }

    private static void listFiles(final File baseDir, ArrayList<File> 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<String> 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() {
    }

    public static List<String> makeClasspath(URL[] urls) {
        List<String> ret = new LinkedList<String>();
        if (urls != null) {
            for (int i = 0; i < urls.length; i++) {
                ret.add(toPathString(urls[i]));
            }
        }
        return ret;
    }

    private static String toPathString(URL url) {
        try {
            return url.toURI().getPath();
        } catch (URISyntaxException e) {
            System.err.println("Warning!! Malformed URL may cause problems: " + url); // TODO: Better way to report this?
            // In this case it was likely not using properly escaped
            // characters so we just use the 'bad' method that doesn't decode
            // special chars
            return url.getPath();
        }
    }

    /**
     * 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) {
        }
    }

}