dk.statsbiblioteket.util.Files.java Source code

Java tutorial

Introduction

Here is the source code for dk.statsbiblioteket.util.Files.java

Source

/* $Id: Files.java,v 1.11 2007/12/04 13:22:01 mke Exp $
 * $Revision: 1.11 $
 * $Date: 2007/12/04 13:22:01 $
 * $Author: mke $
 *
 * The SB Util Library.
 * Copyright (C) 2005-2007  The State and University Library of Denmark
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package dk.statsbiblioteket.util;

import dk.statsbiblioteket.util.qa.QAInfo;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.*;
import java.net.ConnectException;
import java.net.URL;

/**
 * General purpose methods to handle files
 * $Id: Files.java,v 1.11 2007/12/04 13:22:01 mke Exp $
 */
@QAInfo(state = QAInfo.State.QA_NEEDED, level = QAInfo.Level.NORMAL)
public class Files {
    private static Log log = LogFactory.getLog(Files.class);

    // TODO: Add method for recursively copying directories (and just plain file copying)

    /**
     * Enumeration of the two different types of file objects; directories
     * and regular files.
     */
    public enum Type {
        /**
         * A file representing a directory
         */
        directory,

        /**
         * A file representing a regular file
         */
        file
    }

    /**
     * Enumeration of the standard file permission flags
     */
    public enum Permission {
        /**
         * The file is readable
         */
        readable,

        /**
         * The file is writable
         */
        writable,

        /**
         * The file may be run as an executable
         */
        executable
    }

    /**
     * Delete the file or directory given by <code>path</code> (recursively if
     * <code>path</code> is a directory).
     *
     * @param path a {@link File} representing the file or directory to be
     *             deleted.
     * @throws IOException if the path doesn't exist or could not be deleted.
     */
    public static void delete(File path) throws IOException {
        log.trace("delete(" + path + ") called");
        if (!path.exists()) {
            throw new FileNotFoundException(path.toString());
        }
        if (path.isFile()) {
            if (!path.delete()) {
                throw new IOException("Could not delete the file '" + path + "'");
            }
            return;
        }
        for (String child : path.list()) {
            delete(new File(path, child));
        }
        if (!path.delete()) {
            throw new IOException("Could not delete the folder '" + path + "'");
        }
    }

    /**
     * Copy a file or directory (recursively) to a destination path. Generally
     * it behaves as a standard posix command line copy tool.
     *
     * If the input path is a directory and the destination path already
     * exists, the input directory will be copied as a subdir to the destination
     * directory.
     *
     * If the destination directory does not exist it will be created and the
     * contents of the input dire3ctory will be copied here.
     *
     * Note: The recursive copy is not transactional. If an error occurs halfway
     * through the copy, the already copied files will not be removed.
     *
     * @param path      the file or directory to copy from.
     * @param toPath    the destination file or directory.
     * @param overwrite if false this method will throw a
     *                  {@link FileAlreadyExistsException} if the operation will
     *                  overwrite an existing file.
     * @throws IOException                if there was an error copying the file(s)
     * @throws FileAlreadyExistsException if {@code overwrite=false} and the
     *                                    method is about to overwrite an existing file.
     */
    public static void copy(File path, File toPath, boolean overwrite) throws IOException {
        log.trace("copy(" + path + ", " + toPath + ", " + overwrite + ")");
        if (path.isFile()) {
            copyFile(path, toPath, overwrite);
        } else if (path.isDirectory()) {
            if (!toPath.exists()) {
                copyDirectory(path, toPath, overwrite);
            } else {
                copyDirectory(path, new File(toPath, path.getName()), overwrite);
            }
        }
    }

    /**
     * Move a file with the same semantics as the standard Unix {@code move}
     * command.
     *
     * In contrast to the standard Java {@link File#renameTo} this method
     * does extensive sanity checking and throws appropriate exceptions
     * if something is wrong.
     *
     * This method will cause quite a bit of {@code stat} dancing on the
     * file system, so don't use this method in performance critical regions.
     *
     * If {@code dest} is a directory {@code source} will be moved there keeping
     * its base name.
     * If {@code dest} does not exist {@code source} will be renamed to
     * {@code dest}.
     *
     * @param source    a writable file or directory
     * @param dest      either an existing writable directory, or non-existing file
     *                  with existing parent directory
     * @param overwrite if true and {@code dest} exists and is a regular
     *                  file it will be deleted before moving {@code source}
     *                  here
     * @throws FileNotFoundException      if either {@code source} or the parent
     *                                    directory of {@code dest} does not exist
     * @throws FileAlreadyExistsException if {@code dest} exists and is a
     *                                    regular file. If {@code overwrite}
     *                                    is {@code true} this exception will
     *                                    never be thrown
     * @throws FilePermissionException    if {@code source} or {@code dest} is not
     *                                    writable
     * @throws InvalidFileTypeException   if the parent of {@code dest} is a
     *                                    regular file
     * @throws IOException                if there is an unknown error during the move
     *                                    operation
     */
    public static void move(File source, File dest, boolean overwrite) throws IOException {
        if (source == null) {
            throw new NullPointerException("Move source location is null");
        }
        if (dest == null) {
            throw new NullPointerException("Move destination is null");
        }

        /* source checks */
        if (!source.exists()) {
            throw new FileNotFoundException(source.toString());
        }
        if (!source.canWrite()) {
            throw new FilePermissionException(source, Files.Permission.writable);
        }

        /* dest checks */
        File destParent = dest.getParentFile();
        if (dest.exists() && dest.isFile() && !overwrite) {
            throw new FileAlreadyExistsException(dest);
        }
        if (!destParent.exists()) {
            throw new FileNotFoundException("Parent directory of " + dest + " " + "does not exist");
        }
        if (destParent.isFile()) {
            throw new InvalidFileTypeException(destParent, Files.Type.file);
        }
        if (dest.isFile() && !destParent.canWrite()) {
            throw new FilePermissionException(destParent, Files.Permission.writable);
        }

        /* If dest is a dir, move the file into it, keeping the base name */
        if (dest.isDirectory()) {
            if (!dest.canWrite()) {
                throw new FilePermissionException(dest, Files.Permission.writable);
            }
            dest = new File(dest, source.getName());
        }

        if (dest.exists()) {
            if (!overwrite) {
                throw new FileAlreadyExistsException(dest);
            }
            log.trace("Overwriting " + dest);
            Files.delete(dest);
        }

        log.trace("Set to move " + source + " to " + dest);

        // On some platform File.renameTo fails on the first runs. See
        // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6213298
        boolean result = false;
        for (int i = 0; i < 10; i++) {
            result = source.renameTo(dest);
            if (result) {
                break;
            }
            System.gc();
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                // Abort the move operation
                break;
            }
            log.trace("Retrying move (" + i + ")");
        }

        if (!result) {
            log.debug("Atomic move failed. Falling back to copy/delete");
            copy(source, dest, overwrite);
            delete(source);
        }

        log.debug("Moved " + source + " to " + dest);
    }

    /**
     * Copy without clobbering (no overwrite if the destination exists).
     * As {@link #move(File, File, boolean)}, with {@code overwrite} set to {@code false}.
     *
     * @param source source file.
     * @param dest destination file.
     * @throws IOException if the file could not be copied.
     */
    public static void move(File source, File dest) throws IOException {
        move(source, dest, false);
    }

    /*
     * Used in recursive calls for copying directories. Prevents spawning of
     * nested subdirs. Otherwise behaves as {@link #copy}.
     */
    private static void innerCopy(File path, File toPath, boolean overwrite) throws IOException {
        if (path.isFile()) {
            copyFile(path, toPath, overwrite);
        } else if (path.isDirectory()) {
            copyDirectory(path, toPath, overwrite);
        }
    }

    private static void copyDirectory(File path, File toPath, boolean overwrite) throws IOException {
        log.trace("copyDirectory(" + path + ", " + toPath + ", " + overwrite + ") called");
        if (!toPath.exists()) {
            if (!toPath.mkdirs()) {
                throw new IOException("Unable to create or verify the existence" + " of the destination folder '"
                        + toPath.getAbsoluteFile() + "'");
            }
        }
        if (!toPath.canWrite()) {
            throw new IOException("The destination folder '" + toPath.getAbsoluteFile() + "' is not writable");
        }

        for (String filename : path.list()) {
            File in = new File(path, filename);
            File out = new File(toPath, filename);
            innerCopy(in, out, overwrite);
        }
    }

    /**
     * Copies a file (not a folder).
     *
     * @param source      the file to copy.
     * @param destination where to copy the file to. If this is an existing
     *                    directory, {@code source} will be copied into it,
     *                    otherwise {@code source} will copied to this file.
     * @param overwrite   whether or not to overwrite if the destination
     *                    already exists.
     * @throws IOException                thrown if there was an error writing to the
     *                                    destination file, or if the input file doidn't exist
     *                                    or if the source was a directory.
     * @throws FileNotFoundException      thrown if the source file did not exist.
     * @throws FileAlreadyExistsException if there's already a file at
     *                                    {@code destination} and {@code overwrite} was
     *                                    {@code false}.
     */
    private static void copyFile(File source, File destination, boolean overwrite) throws IOException {
        log.trace("copyFile(" + source + ", " + destination + ", " + overwrite + ") called");
        source = source.getAbsoluteFile();
        destination = destination.getAbsoluteFile();
        if (!source.exists()) {
            throw new FileNotFoundException("The source '" + source + "' does not exist");
        }
        if (destination.isDirectory()) {
            throw new IOException("The destination '" + destination + "' is a directory");
        }

        if (destination.exists() && destination.isDirectory()) {
            destination = new File(destination, source.getName());
        }

        if (!overwrite && destination.exists()) {
            throw new FileAlreadyExistsException(destination.toString());
        }

        // BufferedInputStream is not used, as it chokes > 2GB
        InputStream in = new FileInputStream(source);
        OutputStream out = new FileOutputStream(destination);

        try {
            byte[] buf = new byte[2028];
            int count = 0;
            while ((count = in.read(buf)) != -1) {
                out.write(buf, 0, count);
            }
        } finally {
            in.close();
            out.close();
        }
        destination.setExecutable(source.canExecute());
    }

    /**
     * Move the source file or directory recursively to the destination.
     * Generally it behaves as a standard posix command line copy tool,
     * with the addendum that files are moved by performing a complete copy
     * of all files followed by a delete.
     *
     * If the source is a directory and the destination already exists as a
     * directory, the source directory will be copied as a subdir to the
     * destination directory.
     *
     * If the destination directory does not exist it will be created and the
     * contents of the input directory will be copied here.
     *
     * Note: The recursive move is partly transactional. If an error occurs
     *       halfway through the move, the already copied files will not be
     *       removed, but no files from the source will be deleted.
     * @param source      the file or directory to copy from.
     * @param destination the destination file or directory.
     * @param overwrite   if false this method will throw a {@link FileAlreadyExistsException} if the operation
     *                    will overwrite an existing file.
     * @throws IOException if there was an error moving the file(s).
     * @throws FileAlreadyExistsException if {@code overwrite=false} and the method is about to overwrite an
     *                                    existing file.
     */
    /*public static void move(File source, File destination, boolean overwrite) throws IOException {
    copy(source, destination, overwrite);
    delete(source);
    }*/

    /**
     * @param path to file or directory to be deleted
     * @throws java.io.FileNotFoundException if the path doesn't exist
     * @see #delete(java.io.File)
     */
    public static void delete(String path) throws IOException {
        delete(new File(path));
    }

    /**
     * Converts a byte array to String, assuming UTF-8.
     *
     * @param in an array of bytes representing an UTF-8 String.
     * @return the String represented by the byte array.
     */
    private static String bytesToString(byte[] in) {
        try {
            return new String(in, "utf-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("utf-8 not supported", e);
        }
    }

    /*
     * Fetches whatever a given URL points at and returns it as an UTF-8 string
     *
     * @param url The resource to fetch
     * @return The resource as an UTF-8 string
     * @throws IOException
     */
    /*   public static String getTextResource(URL url) throws IOException {
     URLConnection uc = url.openConnection();
     InputStream in = uc.getInputStream();
     ByteArrayOutputStream out = new ByteArrayOutputStream();
     pipeStream(in, out);
     return out.toString("UTF-8");
     }
    */

    /**
     * Store a String on the file system, using UTF-8.
     *
     * @param content     the content to be stored on disk.
     * @param destination where to store the content.
     * @throws IOException if the content could not be stored.
     */
    public static void saveString(String content, File destination) throws IOException {
        log.trace("saveString(String with length " + content.length() + ", " + destination + ") called");
        if (destination.isDirectory()) {
            throw new IOException("The destination '" + destination + "' is a folder, while it should be a file");
        }
        InputStream in = new ByteArrayInputStream(content.getBytes("UTF-8"));
        FileOutputStream out = new FileOutputStream(destination);
        Streams.pipe(in, out);
    }

    /**
     * Read a String from the file system, assuming UTF-8.
     *
     * @param source where to load the String.
     * @return the String as stored in the source.
     * @throws IOException if the String could not be read.
     */
    public static String loadString(File source) throws IOException {
        if (source.isDirectory()) {
            throw new IOException("The source '" + source + "' is a folder, while it should be a file");
        }
        InputStream in = new FileInputStream(source);
        ByteArrayOutputStream out = new ByteArrayOutputStream((int) source.length());
        Streams.pipe(in, out);
        return out.toString("UTF-8");
    }

    /**
     * Return the base name of a file.
     *
     * @param file the file to extract the base name for
     * @return file's basename
     * @deprecated use {@link File#getName()} instead.
     */
    public static String baseName(File file) {
        return file.getName();
    }

    /**
     * Return the base name of a file. For example
     * <code>
     * "autoexec.bat" = baseName ("C:\autoexec.bat")
     * "fstab" = basename ("/etc/fstab")
     * </code>
     *
     * @param filename the filename to extract the base name for.
     * @return file's basename.
     */
    public static String baseName(String filename) {
        return new File(filename).getName();

    }

    /**
     * Download the contents of an {@link URL} and store it on disk.
     *
     * if {@code target} argument is a directory the file will be stored
     * here with the basename as extracted from the url. If it points to
     * a non-existing file it will be written to a file with that name.
     *
     * If the {@code target} argument points to an already existing file
     * and {@code overwrite == false} a {@link FileAlreadyExistsException}
     * will be thrown. Otherwise the file will be overwritten.
     *
     * @param url       where the data should be downloaded from.
     * @param target    the place to store the downloaded data. This can be either a file or a directory.
     * @param overwrite whether or not to overwrite the target file if it already exist.
     * @return the resulting file.
     * @throws ConnectException     if there was an error opening a stream to the url.
     * @throws IOException          if there was an error downloading the file or writing it to disk.
     * @throws NullPointerException if one of the input arguments are null.
     */
    public static File download(URL url, File target, boolean overwrite) throws IOException {
        log.trace("download(" + url + ", " + target + ", " + overwrite + ") called");
        if (url == null) {
            throw new NullPointerException("url is null");
        }
        if (target == null) {
            throw new NullPointerException("target is null");
        }

        File result;

        if (target.isDirectory()) {
            result = new File(target, new File(url.getFile()).getName());
        } else {
            result = target;
        }

        if (result.exists() && !overwrite) {
            throw new FileAlreadyExistsException(target);
        }

        InputStream con;
        try {
            // No BufferedInputStream as it does not support 2GB+.
            con = url.openStream();
        } catch (IOException e) {
            throw new IOException("Failed to open stream to '" + url + "'", e);
        }
        OutputStream out = new FileOutputStream(result);
        Streams.pipe(con, out);
        return result;
    }

    /**
     * See {@link #download(java.net.URL, java.io.File, boolean)}. This method
     * invokes the detailed {@code download} method with {@code overwrite=false}
     *
     * @param url    URL pointing to the data to be downloaded
     * @param target the place to store the downloaded data. A file or directory.
     * @return the resulting file.
     * @throws IOException if there was an error downloading the file or
     *                     writing it to disk.
     */
    public static File download(URL url, File target) throws IOException {
        return download(url, target, false);
    }

}