org.limewire.util.FileUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.limewire.util.FileUtils.java

Source

/*
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.limewire.util;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.Flushable;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Provides file manipulation methods; ensures a file exists, makes a file 
 * writable, renames, saves and deletes a file. 
 */
public class FileUtils {

    private static final Log LOG = LogFactory.getLog(FileUtils.class);

    private static final CopyOnWriteArrayList<FileLocker> fileLockers = new CopyOnWriteArrayList<FileLocker>();

    public static void writeObject(String fileName, Object obj) throws IOException {
        writeObject(new File(fileName), obj);
    }

    /**
     * Writes the passed Object to corresponding file
     * @param file The file to which to write 
     * @param map The Object to be stored
     */
    public static void writeObject(File f, Object obj) throws IOException {

        try {
            f = getCanonicalFile(f);
        } catch (IOException tryAnyway) {
        }

        ObjectOutputStream out = null;
        try {
            //open the file
            out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(f)));
            //write to the file
            out.writeObject(obj);
            out.flush();
        } finally {
            close(out);
        }
    }

    public static Object readObject(String fileName) throws IOException, ClassNotFoundException {
        return readObject(new File(fileName));
    }

    /**
     * @param file The file from where to read the serialized Object
     * @return The Object that was read
     */
    public static Object readObject(File file) throws IOException, ClassNotFoundException {

        try {
            file = getCanonicalFile(file);
        } catch (IOException tryAnyway) {
        }

        ObjectInputStream in = null;
        try {
            //open the file
            in = new ObjectInputStream(new BufferedInputStream(new FileInputStream(file)));
            //read and return the object
            return in.readObject();
        } finally {
            close(in);
        }
    }

    /**
     * Gets the canonical path, catching buggy Windows errors
     */
    public static String getCanonicalPath(File f) throws IOException {
        try {
            return f.getCanonicalPath();
        } catch (IOException ioe) {
            String msg = ioe.getMessage();
            // windows bugs out :(
            if (OSUtils.isWindows() && msg != null && msg.indexOf("There are no more files") != -1)
                return f.getAbsolutePath();
            else
                throw ioe;
        }
    }

    /** Same as f.getCanonicalFile() in JDK1.3. */
    public static File getCanonicalFile(File f) throws IOException {
        try {
            return f.getCanonicalFile();
        } catch (IOException ioe) {
            String msg = ioe.getMessage();
            // windows bugs out :(
            if (OSUtils.isWindows() && msg != null && msg.indexOf("There are no more files") != -1)
                return f.getAbsoluteFile();
            else
                throw ioe;
        }
    }

    /**
     * Determines if file 'a' is an ancestor of file 'b'.
     */
    public static final boolean isAncestor(File a, File b) {
        while (b != null) {
            if (b.equals(a))
                return true;
            b = b.getParentFile();
        }
        return false;
    }

    /** 
     * Detects attempts at directory traversal by testing if testDirectory 
     * really is the parent of testPath.  This method should be used to make
     * sure directory traversal tricks aren't being used to trick
     * LimeWire into reading or writing to unexpected places.
     * 
     * Directory traversal security problems occur when software doesn't 
     * check if input paths contain characters (such as "../") that cause the
     * OS to go up a directory.  This function will ignore benign cases where
     * the path goes up one directory and then back down into the original directory.
     * 
     * @return false if testParent is not the parent of testChild.
     * @throws IOException if getCanonicalPath throws IOException for either input file
     */
    public static final boolean isReallyParent(File testParent, File testChild) throws IOException {
        // Don't check testDirectory.isDirectory... 
        // If it's not a directory, it won't be the parent anyway.
        // This makes the tests more simple.

        String testParentName = getCanonicalPath(testParent);
        String testChildParentName = getCanonicalPath(testChild.getAbsoluteFile().getParentFile());
        if (!testParentName.equals(testChildParentName))
            return false;

        return true;
    }

    /**
     * Detects attempts at directory traversal by testing if testDirectory 
     * really is a parent of testPath.
     * @see isReallyParent
     */
    public static final boolean isReallyInParentPath(File testParent, File testChild) throws IOException {

        String testParentName = getCanonicalPath(testParent);
        File testChildParentFile = testChild.getAbsoluteFile().getParentFile();
        if (testChildParentFile == null)
            testChildParentFile = testChild.getAbsoluteFile();
        String testChildParentName = getCanonicalPath(testChildParentFile);
        return testChildParentName.startsWith(testParentName);
    }

    /**
     * Utility method that returns the file extension of the given file.
     * 
     * @param f the <tt>File</tt> instance from which the extension 
     *   should be extracted
     * @return the file extension string, or <tt>null</tt> if the extension
     *   could not be extracted
     */
    public static String getFileExtension(File f) {
        String name = f.getName();
        return getFileExtension(name);
    }

    /**
     * Utility method that returns the file extension of the given file.
     * 
     * @param name the file name <tt>String</tt> from which the extension
     *  should be extracted
     * @return the file extension string (without the dot), or <tt>null</tt> if the extension
     *   could not be extracted
     */
    public static String getFileExtension(String name) {
        int index = name.lastIndexOf(".");
        if (index == -1) {
            return null;
        }

        // the file must have a name other than the extension
        if (index == 0) {
            return null;
        }

        // if the last character of the string is the ".", then there's
        // no extension
        if (index == (name.length() - 1)) {
            return null;
        }

        return name.substring(index + 1);
    }

    /**
     * Utility method to set a file as non read only.
     * If the file is already writable, does nothing.
     *
     * @param f the <tt>File</tt> instance whose read only flag should
     *  be unset.
     * 
     * @return whether or not <tt>f</tt> is writable after trying to make it
     *  writeable -- note that if the file doesn't exist, then this returns
     *  <tt>true</tt> 
     */
    public static boolean setWriteable(File f) {
        if (!f.exists())
            return true;

        // non Windows-based systems return the wrong value
        // for canWrite when the argument is a directory --
        // writing is based on the 'x' attribute, not the 'w'
        // attribute for directories.
        if (f.canWrite()) {
            if (OSUtils.isWindows())
                return true;
            else if (!f.isDirectory())
                return true;
        }

        String fName;
        try {
            fName = f.getCanonicalPath();
        } catch (IOException ioe) {
            fName = f.getPath();
        }

        String cmds[] = null;
        if (OSUtils.isWindows() || OSUtils.isMacOSX())
            SystemUtils.setWriteable(fName);
        else if (OSUtils.isOS2())
            ;//cmds = null; // Find the right command for OS/2 and fill in
        else {
            if (f.isDirectory())
                cmds = new String[] { "chmod", "u+w+x", fName };
            else
                cmds = new String[] { "chmod", "u+w", fName };
        }

        if (cmds != null) {
            try {
                Process p = Runtime.getRuntime().exec(cmds);
                p.waitFor();
            } catch (SecurityException ignored) {
            } catch (IOException ignored) {
            } catch (InterruptedException ignored) {
            }
        }

        return f.canWrite();
    }

    /**
     * Adds a new FileLocker to the list of FileLockers
     * that are checked when a lock needs to be released
     * on a file prior to deletion or renaming.
     * 
     * @param locker
     */
    public static void addFileLocker(FileLocker locker) {
        fileLockers.addIfAbsent(locker);
    }

    /**
     * Removes <code>locker</code> from the list of FileLockers.
     * 
     * @see #addFileLocker(FileLocker) 
     */
    public static void removeFileLocker(FileLocker locker) {
        fileLockers.remove(locker);
    }

    /**
     * Forcibly renames a file, removing any locks that may
     * be held from any FileLockers that were added.
     * 
     * @param src
     * @param dst
     * @return true if the rename succeeded
     */
    public static boolean forceRename(File src, File dst) {
        // First attempt to rename it.
        boolean success = src.renameTo(dst);

        // If that fails, try releasing the locks one by one.
        if (!success) {
            for (FileLocker locker : fileLockers) {
                if (locker.releaseLock(src)) {
                    success = src.renameTo(dst);
                    if (success)
                        break;
                }
            }
        }

        // If that didn't work, try copying the file.
        if (!success) {
            success = copy(src, dst);
            //if copying succeeded, get rid of the original
            //at this point any active uploads will have been killed
            if (success)
                src.delete();
        }

        return success;
    }

    /**
     * @param directory Gets all files under this directory RECURSIVELY.
     * @param filter If null, then returns all files.  Else, only returns files
     * extensions in the filter array.
     * @return An array of Files recursively obtained from the directory,
     * according to the filter.
     * 
     */
    public static File[] getFilesRecursive(File directory, String[] filter) {
        List<File> dirs = new ArrayList<File>();
        // the return array of files...
        List<File> retFileArray = new ArrayList<File>();
        File[] retArray = new File[0];

        // bootstrap the process
        if (directory.exists() && directory.isDirectory())
            dirs.add(directory);

        // while i have dirs to process
        while (dirs.size() > 0) {
            File currDir = dirs.remove(0);
            String[] listedFiles = currDir.list();
            for (int i = 0; (listedFiles != null) && (i < listedFiles.length); i++) {
                File currFile = new File(currDir, listedFiles[i]);
                if (currFile.isDirectory()) // to be dealt with later
                    dirs.add(currFile);
                else if (currFile.isFile()) { // we have a 'file'....
                    boolean shouldAdd = false;
                    if (filter == null)
                        shouldAdd = true;
                    else {
                        String ext = FileUtils.getFileExtension(currFile);
                        for (int j = 0; (j < filter.length) && (ext != null); j++) {
                            if (ext.equalsIgnoreCase(filter[j])) {
                                shouldAdd = true;

                                // don't keep looping through all filters --
                                // one match is good enough
                                break;
                            }
                        }
                    }
                    if (shouldAdd)
                        retFileArray.add(currFile);
                }
            }
        }

        if (!retFileArray.isEmpty()) {
            retArray = new File[retFileArray.size()];
            for (int i = 0; i < retArray.length; i++)
                retArray[i] = retFileArray.get(i);
        }

        return retArray;
    }

    /**
     * Deletes the given file or directory, moving it to the trash can or 
     * recycle bin if the platform has one and <code>moveToTrash</code> is
     * true.
     * 
     * @param file The file or directory to trash or delete
     * @param moveToTrash whether the file should be moved to the trash bin 
     * or permanently deleted
     * @return     true on success
     * 
     * @throws IllegalArgumentException if the OS does not support moving files
     * to a trash bin, check with {@link OSUtils#supportsTrash()}.
     */
    public static boolean delete(File file, boolean moveToTrash) {
        if (!file.exists()) {
            return false;
        }
        if (moveToTrash) {
            if (OSUtils.isMacOSX()) {
                return moveToTrashOSX(file);
            } else if (OSUtils.isWindows()) {
                return SystemUtils.recycle(file);
            } else {
                throw new IllegalArgumentException("OS does not support trash");
            }
        } else {
            return deleteRecursive(file);
        }
    }

    /**
     * Moves the given file or directory to Trash.
     * 
     * @param file The file or directory to move to Trash
     * @throws IOException if the canonical path cannot be resolved
     *          or if the move process is interrupted
     * @return true on success
     */
    private static boolean moveToTrashOSX(File file) {
        try {
            String[] command = moveToTrashCommand(file);
            ProcessBuilder builder = new ProcessBuilder(command);
            builder.redirectErrorStream();
            Process process = builder.start();
            ProcessUtils.consumeAllInput(process);
            process.waitFor();
        } catch (InterruptedException err) {
            LOG.error("InterruptedException", err);
        } catch (IOException err) {
            LOG.error("IOException", err);
        }
        return !file.exists();
    }

    /**
     * Creates and returns the the osascript command to move
     * a file or directory to the Trash
     * 
     * @param file The file or directory to move to Trash
     * @throws IOException if the canonical path cannot be resolved
     * @return OSAScript command
     */
    private static String[] moveToTrashCommand(File file) {
        String path = null;
        try {
            path = file.getCanonicalPath();
        } catch (IOException err) {
            LOG.error("IOException", err);
            path = file.getAbsolutePath();
        }

        String fileOrFolder = (file.isFile() ? "file" : "folder");

        String[] command = new String[] { "osascript", "-e", "set unixPath to \"" + path + "\"", "-e",
                "set hfsPath to POSIX file unixPath", "-e", "tell application \"Finder\"", "-e",
                "if " + fileOrFolder + " hfsPath exists then", "-e", "move " + fileOrFolder + " hfsPath to trash",
                "-e", "end if", "-e", "end tell" };

        return command;
    }

    /**
     * Deletes all files in 'directory'.
     * Returns true if this succesfully deleted every file recursively, including itself.
     * 
     * @param directory
     * @return
     */
    public static boolean deleteRecursive(File directory) {
        // make sure we only delete canonical children of the parent file we
        // wish to delete. I have a hunch this might be an issue on OSX and
        // Linux under certain circumstances.
        // If anyone can test whether this really happens (possibly related to
        // symlinks), I would much appreciate it.
        String canonicalParent;
        try {
            canonicalParent = getCanonicalPath(directory);
        } catch (IOException ioe) {
            return false;
        }

        if (!directory.isDirectory())
            return directory.delete();

        File[] files = directory.listFiles();
        for (int i = 0; i < files.length; i++) {
            try {
                if (!getCanonicalPath(files[i]).startsWith(canonicalParent))
                    continue;
            } catch (IOException ioe) {
                return false;
            }

            if (!deleteRecursive(files[i]))
                return false;
        }

        return directory.delete();
    }

    /**
     * @return true if the two files are the same.  If they are both
     * directories returns true if there is at least one file that 
     * conflicts.
     */
    public static boolean conflictsAny(File a, File b) {
        if (a.equals(b))
            return true;
        Set<File> unique = new HashSet<File>();
        unique.add(a);
        for (File recursive : getFilesRecursive(a, null))
            unique.add(recursive);

        if (unique.contains(b))
            return true;
        for (File recursive : getFilesRecursive(b, null)) {
            if (unique.contains(recursive))
                return true;
        }

        return false;

    }

    /**
     * Returns total length of all files by going through
     * the given directory (if it's a directory).
     */
    public static long getLengthRecursive(File f) {
        if (!f.isDirectory())
            return f.length();
        long ret = 0;
        for (File file : getFilesRecursive(f, null))
            ret += file.length();
        return ret;
    }

    /**
     * A utility method to close Closeable objects (Readers, Writers, 
     * Input- and OutputStreams and RandomAccessFiles).
     */
    public static void close(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException ignored) {
            }
        }
    }

    /**
     * A utility method to flush Flushable objects (Readers, Writers, 
     * Input- and OutputStreams and RandomAccessFiles).
     */
    public static void flush(Flushable flushable) {
        if (flushable != null) {
            try {
                flushable.flush();
            } catch (IOException ignored) {
            }
        }
    }

    /** 
     * Attempts to copy the first 'amount' bytes of file 'src' to 'dst',
     * returning the number of bytes actually copied.  If 'dst' already exists,
     * the copy may or may not succeed.
     * 
     * @param src the source file to copy
     * @param amount the amount of src to copy, in bytes
     * @param dst the place to copy the file
     * @return the number of bytes actually copied.  Returns 'amount' if the
     *  entire requested range was copied.
     */
    public static long copy(File src, long amount, File dst) {
        final int BUFFER_SIZE = 1024;
        long amountToRead = amount;
        InputStream in = null;
        OutputStream out = null;
        try {
            //I'm not sure whether buffering is needed here.  It can't hurt.
            in = new BufferedInputStream(new FileInputStream(src));
            out = new BufferedOutputStream(new FileOutputStream(dst));
            byte[] buf = new byte[BUFFER_SIZE];
            while (amountToRead > 0) {
                int read = in.read(buf, 0, (int) Math.min(BUFFER_SIZE, amountToRead));
                if (read == -1)
                    break;
                amountToRead -= read;
                out.write(buf, 0, read);
            }
        } catch (IOException e) {
        } finally {
            close(in);
            flush(out);
            close(out);
        }
        return amount - amountToRead;
    }

    /** 
     * Copies the file 'src' to 'dst', returning true iff the copy succeeded.
     * If 'dst' already exists, the copy may or may not succeed.  May also
     * fail for VERY large source files.
     */
    public static boolean copy(File src, File dst) {
        //Downcasting length can result in a sign change, causing
        //copy(File,int,File) to terminate immediately.
        long length = src.length();
        return copy(src, (int) length, dst) == length;
    }

    /**
     * Creates a temporary file using
     * {@link File#createTempFile(String, String, File)}, trying a few times.
     * This is a workaround for Sun Bug: 6325169: createTempFile occasionally
     * fails (throwing an IOException).
     */
    public static File createTempFile(String prefix, String suffix, File directory) throws IOException {
        IOException iox = null;

        for (int i = 0; i < 10; i++) {
            try {
                return File.createTempFile(prefix, suffix, directory);
            } catch (IOException x) {
                iox = x;
            }
        }

        throw iox;
    }

    /**
     * Creates a temporary file using
     * {@link File#createTempFile(String, String)}, trying a few times.
     * This is a workaround for Sun Bug: 6325169: createTempFile occasionally
     * fails (throwing an IOException).
     */
    public static File createTempFile(String prefix, String suffix) throws IOException {
        IOException iox = null;

        for (int i = 0; i < 10; i++) {
            try {
                return File.createTempFile(prefix, suffix);
            } catch (IOException x) {
                iox = x;
            }
        }

        throw iox;
    }

    public static File getJarFromClasspath(String markerFile) {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        if (classLoader == null) {
            classLoader = FileUtils.class.getClassLoader();
        }
        if (classLoader == null) {
            return null;
        }
        return getJarFromClasspath(classLoader, markerFile);
    }

    public static File getJarFromClasspath(ClassLoader classLoader, String markerFile) {
        if (classLoader == null) {
            throw new IllegalArgumentException();
        }

        URL messagesURL = classLoader.getResource(markerFile);
        if (messagesURL != null) {
            String url = CommonUtils.decode(messagesURL.toExternalForm());
            if (url != null && url.startsWith("jar:file:")) {
                url = url.substring("jar:file:".length(), url.length());
                url = url.substring(0, url.length() - markerFile.length() - "!/".length());
                return new File(url);
            }
        }

        return null;
    }

    public static void copyDirectoryRecursively(File srcDir, File targetDir) {
        if (!targetDir.exists()) {
            targetDir.mkdir();
            targetDir.setWritable(true);
        }

        for (File srcElement : srcDir.listFiles()) {
            if (srcElement.isFile()) {
                FileUtils.copy(srcElement, new File(targetDir + File.separator + srcElement.getName()));
            } else if (srcElement.isDirectory()) {
                FileUtils.copyDirectoryRecursively(srcElement, new File(targetDir, srcElement.getName()));
            }
        }
    }

    public static boolean deleteEmptyDirectoryRecursive(File directory) {
        // make sure we only delete canonical children of the parent file we
        // wish to delete. I have a hunch this might be an issue on OSX and
        // Linux under certain circumstances.
        // If anyone can test whether this really happens (possibly related to
        // symlinks), I would much appreciate it.
        String canonicalParent;
        try {
            canonicalParent = getCanonicalPath(directory);
        } catch (IOException ioe) {
            return false;
        }

        if (!directory.isDirectory())
            return false;

        boolean canDelete = true;

        File[] files = directory.listFiles();
        for (int i = 0; i < files.length; i++) {
            try {
                if (!getCanonicalPath(files[i]).startsWith(canonicalParent))
                    continue;
            } catch (IOException ioe) {
                canDelete = false;
            }

            if (!deleteEmptyDirectoryRecursive(files[i])) {
                canDelete = false;
            }
        }

        return canDelete ? directory.delete() : false;
    }
}