com.cinchapi.common.io.Files.java Source code

Java tutorial

Introduction

Here is the source code for com.cinchapi.common.io.Files.java

Source

/*
 * Copyright (c) 2016 Cinchapi Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.cinchapi.common.io;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.util.AbstractList;
import java.util.Iterator;

import javax.annotation.Nullable;

import com.cinchapi.common.base.ArrayBuilder;
import com.cinchapi.common.base.CheckedExceptions;
import com.cinchapi.common.base.Platform;
import com.cinchapi.common.base.ReadOnlyIterator;
import com.cinchapi.common.process.Processes;
import com.cinchapi.common.process.Processes.ProcessResult;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.hash.Hashing;

/**
 * Utility methods for working with files.
 * 
 * @author Jeff Nelson
 */
public final class Files {

    /**
     * Delete {@code directory}. If files are added to the directory while its
     * being deleted, this method will make a best effort to delete those files
     * as well.
     * 
     * @param directory
     */
    public static void deleteDirectory(String directory) {
        try (DirectoryStream<Path> stream = java.nio.file.Files.newDirectoryStream(Paths.get(directory))) {
            for (Path path : stream) {
                if (java.nio.file.Files.isDirectory(path)) {
                    deleteDirectory(path.toString());
                } else {
                    java.nio.file.Files.delete(path);
                }
            }
            java.nio.file.Files.delete(Paths.get(directory));
        } catch (IOException e) {
            if (e.getClass() == DirectoryNotEmptyException.class) {
                deleteDirectory(directory);
            } else {
                throw CheckedExceptions.wrapAsRuntimeException(e);
            }
        }
    }

    /**
     * Return the checksum for all the files in the {@code directory}.
     * 
     * @param directory the directory whose checksum is generated
     * @return the checksum of the directory
     */
    public static String directoryChecksum(String directory) {
        String hash = Platform.isMacOsX() ? "shasum -a 256" : "sha256sum";
        try {
            Process process = Processes
                    .getBuilderWithPipeSupport("find . -type f -exec " + hash + " --binary {} \\; | sort -k 2 | "
                            + hash + " | cut -d ' ' -f 1")
                    .directory(Paths.get(expandPath(directory)).toFile()).start();
            ProcessResult result = Processes.waitFor(process);
            if (result.exitCode() == 0) {
                return result.out().get(0);
            } else {
                throw new RuntimeException(result.err().toString());
            }
        } catch (IOException e) {
            throw CheckedExceptions.throwAsRuntimeException(e);
        }

    }

    /**
     * A version of the {@link #directoryChecksum(String)} method that uses
     * metadata caching for the {@code directory} to speed up checksum
     * calculations, where possible.
     * <p>
     * Do not use this method for security operations because there are subtle
     * race conditions and possible exploits that can happen when using metadata
     * caching. However, this method is appropriate to use for validating the
     * integrity of files in non-critical situations or those where the
     * likelihood of tampering is low.
     * </p>
     * 
     * @param directory the directory whose checksum is generated
     * @return the checksum of the directory
     */
    public static String directoryChecksumCached(String directory) {
        Path cache = Paths.get(USER_HOME).resolve(Paths.get(".cinchapi", "accent4j", ".checksums",
                Hashing.sha256().hashString(directory, StandardCharsets.UTF_8).toString()));
        File file = cache.toFile();
        try {
            String checksum;
            long directoryLastModified = getMostRecentFileModifiedTime(directory).toMillis();
            if (file.exists()
                    && getMostRecentFileModifiedTime(cache.toString()).toMillis() > directoryLastModified) {
                checksum = Iterables.getOnlyElement(readLines(cache.toString()));
            } else {
                checksum = directoryChecksum(directory);
                Thread.sleep(1000); // avoid race condition with file
                                    // modification timestamps because many file
                                    // systems only have granularity within 1
                                    // second.
                file.getParentFile().mkdirs();
                file.createNewFile();
                com.google.common.io.Files.asCharSink(file, StandardCharsets.UTF_8).write(checksum);
            }
            return checksum;

        } catch (IOException | InterruptedException e) {
            throw CheckedExceptions.throwAsRuntimeException(e);
        }
    }

    /**
     * Expand the given {@code path} so that it contains completely normalized
     * components (e.g. ".", "..", and "~" are resolved to the correct absolute
     * paths).
     * 
     * @param path
     * @return the expanded path
     */
    public static String expandPath(String path) {
        return expandPath(path, null);
    }

    /**
     * Expand the given {@code path} so that it contains completely normalized
     * components (e.g. ".", "..", and "~" are resolved to the correct absolute
     * paths).
     * 
     * @param path
     * @param cwd
     * @return the expanded path
     */
    public static String expandPath(String path, String cwd) {
        path = path.replaceAll("~", USER_HOME);
        Path base = cwd == null || cwd.isEmpty() ? BASE_PATH : FileSystems.getDefault().getPath(cwd);
        return base.resolve(path).normalize().toString();
    }

    /**
     * Get a consistent file path that represents the hash for the specified
     * {@code key}.
     * <p>
     * This method does <strong>NOT</strong> create a file at the returned path,
     * or any of the parent directories.
     * </p>
     * 
     * @param key the key to hash
     * @return the hashed file path
     */
    public static Path getHashedFilePath(String key) {
        String hash = Hashing.sha256().hashString(key, StandardCharsets.UTF_8).toString();
        ArrayBuilder<String> array = ArrayBuilder.builder();
        array.add(".hash");
        StringBuilder sb = new StringBuilder();
        char[] chars = hash.toCharArray();
        for (int i = 0; i < chars.length; ++i) {
            char c = chars[i];
            sb.append(c);
            if (i >= 4 && i % 4 == 0) {
                array.add(sb.toString());
                sb.setLength(0);
            }
        }
        return Paths.get(USER_HOME, array.build());
    }

    /**
     * Get the the modified time of the most recently changed file within the
     * {@code directory}. Please note that this method differs from other
     * methods that check the modified timestamp of the directory itself as
     * opposed to checking the modified timestamps of the contents of the
     * directory.
     * <p>
     * This method will recursively search subdirectories for file modifications
     * as well.
     * </p>
     * 
     * @param directory the directory to check
     * @return the modified timestamp of the most recently updated file
     */
    public static FileTime getMostRecentFileModifiedTime(String directory) {
        Path path = Paths.get(directory);
        File file = path.toFile();
        FileTime recent = FileTime.fromMillis(0);
        if (file.isDirectory()) {
            for (File f : file.listFiles()) {
                FileTime proposed = f.isDirectory() ? getMostRecentFileModifiedTime(f.getAbsolutePath())
                        : FileTime.fromMillis(f.lastModified());
                if (proposed.compareTo(recent) > 0) {
                    recent = proposed;
                }
            }
            return recent;
        } else {
            return FileTime.fromMillis(file.lastModified());
        }
    }

    /**
     * Get a consistent temporary directory file path that represents the hash
     * for the specified {@code key}.
     * <p>
     * This method does <strong>NOT</strong> create a file at the returned path,
     * or any of the parent directories.
     * </p>
     * 
     * @param key the key to hash
     * @return the hashed file path
     */
    public static Path getTemporaryHashedFilePath(String key) {
        String hash = Hashing.sha256().hashString(key, StandardCharsets.UTF_8).toString();
        ArrayBuilder<String> array = ArrayBuilder.builder();
        StringBuilder sb = new StringBuilder();
        char[] chars = hash.toCharArray();
        for (int i = 0; i < chars.length; ++i) {
            char c = chars[i];
            sb.append(c);
            if (i >= 4 && i % 4 == 0) {
                array.add(sb.toString());
                sb.setLength(0);
            }
        }
        return Paths.get(TMPDIR.toAbsolutePath().toString(), array.build());
    }

    /**
     * Return an {@link Iterable} collection that lazily accumulates lines in
     * the underlying {@code file}.
     * <p>
     * This method is really just syntactic sugar for reading lines from a file,
     * so the returned collection doesn't actually allow any operations other
     * than forward iteration.
     * </p>
     * 
     * @param file the path to the file
     * @return an iterable collection of lines from the file
     */
    public static Iterable<String> readLines(final String file) {
        return readLines(file, null);
    }

    /**
     * Return an {@link Iterable} collection that lazily accumulates lines in
     * the underlying {@code file}.
     * <p>
     * This method is really just syntactic sugar for reading lines from a file,
     * so the returned collection doesn't actually allow any operations other
     * than forward iteration.
     * </p>
     * 
     * @param file the path to the file
     * @param cwd the working directory against which relative paths are
     *            resolved
     * @return an iterable collection of lines in the file
     */
    public static Iterable<String> readLines(final String file, @Nullable String cwd) {
        final String rwd = cwd == null ? WORKING_DIRECTORY : cwd;
        return new AbstractList<String>() {

            @Override
            public String get(int index) {
                throw new UnsupportedOperationException();
            }

            @Override
            public Iterator<String> iterator() {
                return new ReadOnlyIterator<String>() {

                    String line = null;
                    BufferedReader reader;
                    {
                        try {
                            reader = new BufferedReader(new FileReader(expandPath(file, rwd)));
                            line = reader.readLine();
                        } catch (IOException e) {
                            throw CheckedExceptions.throwAsRuntimeException(e);
                        }
                    }

                    @Override
                    public boolean hasNext() {
                        return this.line != null;
                    }

                    @Override
                    public String next() {
                        String result = line;
                        try {
                            line = reader.readLine();
                            if (line == null) {
                                reader.close();
                            }
                            return result;
                        } catch (IOException e) {
                            throw CheckedExceptions.throwAsRuntimeException(e);
                        }
                    }

                };
            }

            @Override
            public int size() {
                int size = 0;
                Iterator<String> it = iterator();
                while (it.hasNext()) {
                    size += 1;
                    it.next();
                }
                return size;
            }

        };

    }

    /**
     * Create a temporary directory with the specified {@code prefix}.
     * 
     * @param prefix the directory name prefix
     * @return the path to the temporary directory
     */
    public static String tempDir(String prefix) {
        try {
            return java.nio.file.Files.createTempDirectory(prefix).toString();
        } catch (IOException e) {
            throw CheckedExceptions.wrapAsRuntimeException(e);
        }
    }

    /**
     * Implementation of {@link java.nio.file.Files#newDirectoryStream(Path)}
     * that returns an empty stream if {@code dir} does not exist.
     * 
     * @param dir
     * @return the {@link DirectoryStream}
     * @throws IOException
     */
    public static DirectoryStream<Path> newDirectoryStreamNotExistsSafe(Path dir) throws IOException {
        if (dir.toFile().exists()) {
            return java.nio.file.Files.newDirectoryStream(dir);
        } else {
            return new DirectoryStream<Path>() {

                @Override
                public void close() throws IOException {
                }

                @Override
                public Iterator<Path> iterator() {
                    return Iterators.forArray();
                }

            };
        }
    }

    /**
     * Return the base temporary directory that is used by the JVM without
     * throwing a checked exception.
     * 
     * @return the base tmpdir
     */
    private static Path getBaseTemporaryDirectory() {
        try {
            return java.nio.file.Files.createTempFile("cnch", ".a4j").getParent();
        } catch (IOException e) {
            throw CheckedExceptions.wrapAsRuntimeException(e);
        }
    }

    /**
     * The base temporary directory used by the JVM.
     */
    private final static Path TMPDIR = getBaseTemporaryDirectory();

    /**
     * The user's home directory, which is used to expand path names with "~"
     * (tilde).
     */
    private static String USER_HOME = System.getProperty("user.home");

    /**
     * The working directory from which the current JVM process was launched.
     */
    private static String WORKING_DIRECTORY = System.getProperty("user.dir");

    /**
     * The base path that is used to resolve and normalize other relative paths.
     */
    private static Path BASE_PATH = FileSystems.getDefault().getPath(WORKING_DIRECTORY);

}