    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017
    018package org.apache.commons.io.file;
    019
    020import java.io.IOException;
    021import java.io.InputStream;
    022import java.net.URI;
    023import java.net.URL;
    024import java.nio.file.CopyOption;
    025import java.nio.file.DirectoryStream;
    026import java.nio.file.FileVisitOption;
    027import java.nio.file.FileVisitor;
    028import java.nio.file.Files;
    029import java.nio.file.LinkOption;
    030import java.nio.file.NoSuchFileException;
    031import java.nio.file.OpenOption;
    032import java.nio.file.Path;
    033import java.nio.file.Paths;
    034import java.nio.file.attribute.AclEntry;
    035import java.nio.file.attribute.AclFileAttributeView;
    036import java.nio.file.attribute.DosFileAttributeView;
    037import java.nio.file.attribute.PosixFileAttributeView;
    038import java.nio.file.attribute.PosixFileAttributes;
    039import java.nio.file.attribute.PosixFilePermission;
    040import java.util.Arrays;
    041import java.util.Collection;
    042import java.util.Collections;
    043import java.util.Comparator;
    044import java.util.EnumSet;
    045import java.util.List;
    046import java.util.Set;
    047import java.util.stream.Collectors;
    048import java.util.stream.Stream;
    049
    050import org.apache.commons.io.IOUtils;
    051import org.apache.commons.io.file.Counters.PathCounters;
    052
    053/**
    054 * NIO Path utilities.
    055 *
    056 * @since 2.7
    057 */
    058public final class PathUtils {
    059
    060    /**
    061     * Private worker/holder that computes and tracks relative path names and their equality. We reuse the sorted
    062     * relative lists when comparing directories.
    063     */
    064    private static class RelativeSortedPaths {
    065
    066        final boolean equals;
    067        // final List<Path> relativeDirList1; // might need later?
    068        // final List<Path> relativeDirList2; // might need later?
    069        final List<Path> relativeFileList1;
    070        final List<Path> relativeFileList2;
    071
    072        /**
    073         * Constructs and initializes a new instance by accumulating directory and file info.
    074         *
    075         * @param dir1 First directory to compare.
    076         * @param dir2 Seconds directory to compare.
    077         * @param maxDepth See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
    078         * @param linkOptions Options indicating how symbolic links are handled.
    079         * @param fileVisitOptions See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
    080         * @throws IOException if an I/O error is thrown by a visitor method.
    081         */
    082        private RelativeSortedPaths(final Path dir1, final Path dir2, final int maxDepth,
    083            final LinkOption[] linkOptions, final FileVisitOption[] fileVisitOptions) throws IOException {
    084            List<Path> tmpRelativeDirList1 = null;
    085            List<Path> tmpRelativeDirList2 = null;
    086            List<Path> tmpRelativeFileList1 = null;
    087            List<Path> tmpRelativeFileList2 = null;
    088            if (dir1 == null && dir2 == null) {
    089                equals = true;
    090            } else if (dir1 == null ^ dir2 == null) {
    091                equals = false;
    092            } else {
    093                final boolean parentDirExists1 = Files.exists(dir1, linkOptions);
    094                final boolean parentDirExists2 = Files.exists(dir2, linkOptions);
    095                if (!parentDirExists1 || !parentDirExists2) {
    096                    equals = !parentDirExists1 && !parentDirExists2;
    097                } else {
    098                    final AccumulatorPathVisitor visitor1 = accumulate(dir1, maxDepth, fileVisitOptions);
    099                    final AccumulatorPathVisitor visitor2 = accumulate(dir2, maxDepth, fileVisitOptions);
    100                    if (visitor1.getDirList().size() != visitor2.getDirList().size()
    101                        || visitor1.getFileList().size() != visitor2.getFileList().size()) {
    102                        equals = false;
    103                    } else {
    104                        tmpRelativeDirList1 = visitor1.relativizeDirectories(dir1, true, null);
    105                        tmpRelativeDirList2 = visitor2.relativizeDirectories(dir2, true, null);
    106                        if (!tmpRelativeDirList1.equals(tmpRelativeDirList2)) {
    107                            equals = false;
    108                        } else {
    109                            tmpRelativeFileList1 = visitor1.relativizeFiles(dir1, true, null);
    110                            tmpRelativeFileList2 = visitor2.relativizeFiles(dir2, true, null);
    111                            equals = tmpRelativeFileList1.equals(tmpRelativeFileList2);
    112                        }
    113                    }
    114                }
    115            }
    116            // relativeDirList1 = tmpRelativeDirList1;
    117            // relativeDirList2 = tmpRelativeDirList2;
    118            relativeFileList1 = tmpRelativeFileList1;
    119            relativeFileList2 = tmpRelativeFileList2;
    120        }
    121    }
    122
    123    /**
    124     * Empty {@link LinkOption} array.
    125     *
    126     * @since 2.8.0
    127     */
    128    public static final DeleteOption[] EMPTY_DELETE_OPTION_ARRAY = new DeleteOption[0];
    129
    130    /**
    131     * Empty {@link FileVisitOption} array.
    132     */
    133    public static final FileVisitOption[] EMPTY_FILE_VISIT_OPTION_ARRAY = new FileVisitOption[0];
    134
    135    /**
    136     * Empty {@link LinkOption} array.
    137     */
    138    public static final LinkOption[] EMPTY_LINK_OPTION_ARRAY = new LinkOption[0];
    139
    140    /**
    141     * Empty {@link OpenOption} array.
    142     */
    143    public static final OpenOption[] EMPTY_OPEN_OPTION_ARRAY = new OpenOption[0];
    144
    145    /**
    146     * Accumulates file tree information in a {@link AccumulatorPathVisitor}.
    147     *
    148     * @param directory The directory to accumulate information.
    149     * @param maxDepth See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
    150     * @param fileVisitOptions See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
    151     * @throws IOException if an I/O error is thrown by a visitor method.
    152     * @return file tree information.
    153     */
    154    private static AccumulatorPathVisitor accumulate(final Path directory, final int maxDepth,
    155        final FileVisitOption[] fileVisitOptions) throws IOException {
    156        return visitFileTree(AccumulatorPathVisitor.withLongCounters(), directory,
    157            toFileVisitOptionSet(fileVisitOptions), maxDepth);
    158    }
    159
    160    /**
    161     * Cleans a directory including sub-directories without deleting directories.
    162     *
    163     * @param directory directory to clean.
    164     * @return The visitation path counters.
    165     * @throws IOException if an I/O error is thrown by a visitor method.
    166     */
    167    public static PathCounters cleanDirectory(final Path directory) throws IOException {
    168        return cleanDirectory(directory, EMPTY_DELETE_OPTION_ARRAY);
    169    }
    170
    171    /**
    172     * Cleans a directory including sub-directories without deleting directories.
    173     *
    174     * @param directory directory to clean.
    175     * @param options options indicating how deletion is handled.
    176     * @return The visitation path counters.
    177     * @throws IOException if an I/O error is thrown by a visitor method.
    178     * @since 2.8.0
    179     */
    180    public static PathCounters cleanDirectory(final Path directory, final DeleteOption... options) throws IOException {
    181        return visitFileTree(new CleaningPathVisitor(Counters.longPathCounters(), options), directory)
    182            .getPathCounters();
    183    }
    184
    185    /**
    186     * Copies a directory to another directory.
    187     *
    188     * @param sourceDirectory The source directory.
    189     * @param targetDirectory The target directory.
    190     * @param copyOptions Specifies how the copying should be done.
    191     * @return The visitation path counters.
    192     * @throws IOException if an I/O error is thrown by a visitor method.
    193     */
    194    public static PathCounters copyDirectory(final Path sourceDirectory, final Path targetDirectory,
    195        final CopyOption... copyOptions) throws IOException {
    196        return visitFileTree(
    197            new CopyDirectoryVisitor(Counters.longPathCounters(), sourceDirectory, targetDirectory, copyOptions),
    198            sourceDirectory).getPathCounters();
    199    }
    200
    201    /**
    202     * Copies a URL to a directory.
    203     *
    204     * @param sourceFile The source URL.
    205     * @param targetFile The target file.
    206     * @param copyOptions Specifies how the copying should be done.
    207     * @return The target file
    208     * @throws IOException if an I/O error occurs
    209     * @see Files#copy(InputStream, Path, CopyOption...)
    210     */
    211    public static Path copyFile(final URL sourceFile, final Path targetFile, final CopyOption... copyOptions)
    212        throws IOException {
    213        try (final InputStream inputStream = sourceFile.openStream()) {
    214            Files.copy(inputStream, targetFile, copyOptions);
    215            return targetFile;
    216        }
    217    }
    218
    219    /**
    220     * Copies a file to a directory.
    221     *
    222     * @param sourceFile The source file.
    223     * @param targetDirectory The target directory.
    224     * @param copyOptions Specifies how the copying should be done.
    225     * @return The target file
    226     * @throws IOException if an I/O error occurs
    227     * @see Files#copy(Path, Path, CopyOption...)
    228     */
    229    public static Path copyFileToDirectory(final Path sourceFile, final Path targetDirectory,
    230        final CopyOption... copyOptions) throws IOException {
    231        return Files.copy(sourceFile, targetDirectory.resolve(sourceFile.getFileName()), copyOptions);
    232    }
    233
    234    /**
    235     * Copies a URL to a directory.
    236     *
    237     * @param sourceFile The source URL.
    238     * @param targetDirectory The target directory.
    239     * @param copyOptions Specifies how the copying should be done.
    240     * @return The target file
    241     * @throws IOException if an I/O error occurs
    242     * @see Files#copy(InputStream, Path, CopyOption...)
    243     */
    244    public static Path copyFileToDirectory(final URL sourceFile, final Path targetDirectory,
    245        final CopyOption... copyOptions) throws IOException {
    246        try (final InputStream inputStream = sourceFile.openStream()) {
    247            Files.copy(inputStream, targetDirectory.resolve(sourceFile.getFile()), copyOptions);
    248            return targetDirectory;
    249        }
    250    }
    251
    252    /**
    253     * Counts aspects of a directory including sub-directories.
    254     *
    255     * @param directory directory to delete.
    256     * @return The visitor used to count the given directory.
    257     * @throws IOException if an I/O error is thrown by a visitor method.
    258     */
    259    public static PathCounters countDirectory(final Path directory) throws IOException {
    260        return visitFileTree(new CountingPathVisitor(Counters.longPathCounters()), directory).getPathCounters();
    261    }
    262
    263    /**
    264     * Deletes a file or directory. If the path is a directory, delete it and all sub-directories.
    265     * <p>
    266     * The difference between File.delete() and this method are:
    267     * </p>
    268     * <ul>
    269     * <li>A directory to delete does not have to be empty.</li>
    270     * <li>You get exceptions when a file or directory cannot be deleted; {@link java.io.File#delete()} returns a
    271     * boolean.
    272     * </ul>
    273     *
    274     * @param path file or directory to delete, must not be {@code null}
    275     * @return The visitor used to delete the given directory.
    276     * @throws NullPointerException if the directory is {@code null}
    277     * @throws IOException if an I/O error is thrown by a visitor method or if an I/O error occurs.
    278     */
    279    public static PathCounters delete(final Path path) throws IOException {
    280        return delete(path, EMPTY_DELETE_OPTION_ARRAY);
    281    }
    282
    283    /**
    284     * Deletes a file or directory. If the path is a directory, delete it and all sub-directories.
    285     * <p>
    286     * The difference between File.delete() and this method are:
    287     * </p>
    288     * <ul>
    289     * <li>A directory to delete does not have to be empty.</li>
    290     * <li>You get exceptions when a file or directory cannot be deleted; {@link java.io.File#delete()} returns a
    291     * boolean.
    292     * </ul>
    293     *
    294     * @param path file or directory to delete, must not be {@code null}
    295     * @param options options indicating how deletion is handled.
    296     * @return The visitor used to delete the given directory.
    297     * @throws NullPointerException if the directory is {@code null}
    298     * @throws IOException if an I/O error is thrown by a visitor method or if an I/O error occurs.
    299     * @since 2.8.0
    300     */
    301    public static PathCounters delete(final Path path, final DeleteOption... options) throws IOException {
    302        // File deletion through Files deletes links, not targets, so use LinkOption.NOFOLLOW_LINKS.
    303        return Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS) ? deleteDirectory(path, options)
    304            : deleteFile(path, options);
    305    }
    306
    307    /**
    308     * Deletes a directory including sub-directories.
    309     *
    310     * @param directory directory to delete.
    311     * @return The visitor used to delete the given directory.
    312     * @throws IOException if an I/O error is thrown by a visitor method.
    313     */
    314    public static PathCounters deleteDirectory(final Path directory) throws IOException {
    315        return deleteDirectory(directory, EMPTY_DELETE_OPTION_ARRAY);
    316    }
    317
    318    /**
    319     * Deletes a directory including sub-directories.
    320     *
    321     * @param directory directory to delete.
    322     * @param options options indicating how deletion is handled.
    323     * @return The visitor used to delete the given directory.
    324     * @throws IOException if an I/O error is thrown by a visitor method.
    325     * @since 2.8.0
    326     */
    327    public static PathCounters deleteDirectory(final Path directory, final DeleteOption... options) throws IOException {
    328        return visitFileTree(new DeletingPathVisitor(Counters.longPathCounters(), options), directory)
    329            .getPathCounters();
    330    }
    331
    332    /**
    333     * Deletes the given file.
    334     *
    335     * @param file The file to delete.
    336     * @return A visitor with path counts set to 1 file, 0 directories, and the size of the deleted file.
    337     * @throws IOException if an I/O error occurs.
    338     * @throws NoSuchFileException if the file is a directory.
    339     */
    340    public static PathCounters deleteFile(final Path file) throws IOException {
    341        return deleteFile(file, EMPTY_DELETE_OPTION_ARRAY);
    342    }
    343
    344    /**
    345     * Deletes the given file.
    346     *
    347     * @param file The file to delete.
    348     * @param options options indicating how deletion is handled.
    349     * @return A visitor with path counts set to 1 file, 0 directories, and the size of the deleted file.
    350     * @throws IOException if an I/O error occurs.
    351     * @throws NoSuchFileException if the file is a directory.
    352     * @since 2.8.0
    353     */
    354    public static PathCounters deleteFile(final Path file, final DeleteOption... options) throws IOException {
    355        // Files.deleteIfExists() never follows links, so use LinkOption.NOFOLLOW_LINKS in other calls to Files.
    356        if (Files.isDirectory(file, LinkOption.NOFOLLOW_LINKS)) {
    357            throw new NoSuchFileException(file.toString());
    358        }
    359        final PathCounters pathCounts = Counters.longPathCounters();
    360        final boolean exists = Files.exists(file, LinkOption.NOFOLLOW_LINKS);
    361        final long size = exists ? Files.size(file) : 0;
    362        if (overrideReadOnly(options) && exists) {
    363            setReadOnly(file, false, LinkOption.NOFOLLOW_LINKS);
    364        }
    365        if (Files.deleteIfExists(file)) {
    366            pathCounts.getFileCounter().increment();
    367            pathCounts.getByteCounter().add(size);
    368        }
    369        return pathCounts;
    370    }
    371
    372    /**
    373     * Returns true if the given options contain {@link StandardDeleteOption#OVERRIDE_READ_ONLY}.
    374     *
    375     * @param options the array to test
    376     * @return true if the given options contain {@link StandardDeleteOption#OVERRIDE_READ_ONLY}.
    377     */
    378    private static boolean overrideReadOnly(final DeleteOption[] options) {
    379        if (options == null) {
    380            return false;
    381        }
    382        for (final DeleteOption deleteOption : options) {
    383            if (deleteOption == StandardDeleteOption.OVERRIDE_READ_ONLY) {
    384                return true;
    385            }
    386        }
    387        return false;
    388    }
    389
    390    /**
    391     * Compares the file sets of two Paths to determine if they are equal or not while considering file contents. The
    392     * comparison includes all files in all sub-directories.
    393     *
    394     * @param path1 The first directory.
    395     * @param path2 The second directory.
    396     * @return Whether the two directories contain the same files while considering file contents.
    397     * @throws IOException if an I/O error is thrown by a visitor method
    398     */
    399    public static boolean directoryAndFileContentEquals(final Path path1, final Path path2) throws IOException {
    400        return directoryAndFileContentEquals(path1, path2, EMPTY_LINK_OPTION_ARRAY, EMPTY_OPEN_OPTION_ARRAY,
    401            EMPTY_FILE_VISIT_OPTION_ARRAY);
    402    }
    403
    404    /**
    405     * Compares the file sets of two Paths to determine if they are equal or not while considering file contents. The
    406     * comparison includes all files in all sub-directories.
    407     *
    408     * @param path1 The first directory.
    409     * @param path2 The second directory.
    410     * @param linkOptions options to follow links.
    411     * @param openOptions options to open files.
    412     * @param fileVisitOption options to configure traversal.
    413     * @return Whether the two directories contain the same files while considering file contents.
    414     * @throws IOException if an I/O error is thrown by a visitor method
    415     */
    416    public static boolean directoryAndFileContentEquals(final Path path1, final Path path2,
    417        final LinkOption[] linkOptions, final OpenOption[] openOptions, final FileVisitOption[] fileVisitOption)
    418        throws IOException {
    419        // First walk both file trees and gather normalized paths.
    420        if (path1 == null && path2 == null) {
    421            return true;
    422        }
    423        if (path1 == null ^ path2 == null) {
    424            return false;
    425        }
    426        if (!Files.exists(path1) && !Files.exists(path2)) {
    427            return true;
    428        }
    429        final RelativeSortedPaths relativeSortedPaths = new RelativeSortedPaths(path1, path2, Integer.MAX_VALUE,
    430            linkOptions, fileVisitOption);
    431        // If the normalized path names and counts are not the same, no need to compare contents.
    432        if (!relativeSortedPaths.equals) {
    433            return false;
    434        }
    435        // Both visitors contain the same normalized paths, we can compare file contents.
    436        final List<Path> fileList1 = relativeSortedPaths.relativeFileList1;
    437        final List<Path> fileList2 = relativeSortedPaths.relativeFileList2;
    438        for (final Path path : fileList1) {
    439            final int binarySearch = Collections.binarySearch(fileList2, path);
    440            if (binarySearch > -1) {
    441                if (!fileContentEquals(path1.resolve(path), path2.resolve(path), linkOptions, openOptions)) {
    442                    return false;
    443                }
    444            } else {
    445                throw new IllegalStateException("Unexpected mismatch.");
    446            }
    447        }
    448        return true;
    449    }
    450
    451    /**
    452     * Compares the file sets of two Paths to determine if they are equal or not without considering file contents. The
    453     * comparison includes all files in all sub-directories.
    454     *
    455     * @param path1 The first directory.
    456     * @param path2 The second directory.
    457     * @return Whether the two directories contain the same files without considering file contents.
    458     * @throws IOException if an I/O error is thrown by a visitor method
    459     */
    460    public static boolean directoryContentEquals(final Path path1, final Path path2) throws IOException {
    461        return directoryContentEquals(path1, path2, Integer.MAX_VALUE, EMPTY_LINK_OPTION_ARRAY,
    462            EMPTY_FILE_VISIT_OPTION_ARRAY);
    463    }
    464
    465    /**
    466     * Compares the file sets of two Paths to determine if they are equal or not without considering file contents. The
    467     * comparison includes all files in all sub-directories.
    468     *
    469     * @param path1 The first directory.
    470     * @param path2 The second directory.
    471     * @param maxDepth See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
    472     * @param linkOptions options to follow links.
    473     * @param fileVisitOptions options to configure the traversal
    474     * @return Whether the two directories contain the same files without considering file contents.
    475     * @throws IOException if an I/O error is thrown by a visitor method
    476     */
    477    public static boolean directoryContentEquals(final Path path1, final Path path2, final int maxDepth,
    478        final LinkOption[] linkOptions, final FileVisitOption[] fileVisitOptions) throws IOException {
    479        return new RelativeSortedPaths(path1, path2, maxDepth, linkOptions, fileVisitOptions).equals;
    480    }
    481
    482    /**
    483     * Compares the file contents of two Paths to determine if they are equal or not.
    484     * <p>
    485     * File content is accessed through {@link Files#newInputStream(Path,OpenOption...)}.
    486     * </p>
    487     *
    488     * @param path1 the first stream.
    489     * @param path2 the second stream.
    490     * @return true if the content of the streams are equal or they both don't exist, false otherwise.
    491     * @throws NullPointerException if either input is null.
    492     * @throws IOException if an I/O error occurs.
    493     * @see org.apache.commons.io.FileUtils#contentEquals(java.io.File, java.io.File)
    494     */
    495    public static boolean fileContentEquals(final Path path1, final Path path2) throws IOException {
    496        return fileContentEquals(path1, path2, EMPTY_LINK_OPTION_ARRAY, EMPTY_OPEN_OPTION_ARRAY);
    497    }
    498
    499    /**
    500     * Compares the file contents of two Paths to determine if they are equal or not.
    501     * <p>
    502     * File content is accessed through {@link Files#newInputStream(Path,OpenOption...)}.
    503     * </p>
    504     *
    505     * @param path1 the first stream.
    506     * @param path2 the second stream.
    507     * @param linkOptions options specifying how files are followed.
    508     * @param openOptions options specifying how files are opened.
    509     * @return true if the content of the streams are equal or they both don't exist, false otherwise.
    510     * @throws NullPointerException if either input is null.
    511     * @throws IOException if an I/O error occurs.
    512     * @see org.apache.commons.io.FileUtils#contentEquals(java.io.File, java.io.File)
    513     */
    514    public static boolean fileContentEquals(final Path path1, final Path path2, final LinkOption[] linkOptions,
    515        final OpenOption[] openOptions) throws IOException {
    516        if (path1 == null && path2 == null) {
    517            return true;
    518        }
    519        if (path1 == null ^ path2 == null) {
    520            return false;
    521        }
    522        final Path nPath1 = path1.normalize();
    523        final Path nPath2 = path2.normalize();
    524        final boolean path1Exists = Files.exists(nPath1, linkOptions);
    525        if (path1Exists != Files.exists(nPath2, linkOptions)) {
    526            return false;
    527        }
    528        if (!path1Exists) {
    529            // Two not existing files are equal?
    530            // Same as FileUtils
    531            return true;
    532        }
    533        if (Files.isDirectory(nPath1, linkOptions)) {
    534            // don't compare directory contents.
    535            throw new IOException("Can't compare directories, only files: " + nPath1);
    536        }
    537        if (Files.isDirectory(nPath2, linkOptions)) {
    538            // don't compare directory contents.
    539            throw new IOException("Can't compare directories, only files: " + nPath2);
    540        }
    541        if (Files.size(nPath1) != Files.size(nPath2)) {
    542            // lengths differ, cannot be equal
    543            return false;
    544        }
    545        if (path1.equals(path2)) {
    546            // same file
    547            return true;
    548        }
    549        try (final InputStream inputStream1 = Files.newInputStream(nPath1, openOptions);
    550            final InputStream inputStream2 = Files.newInputStream(nPath2, openOptions)) {
    551            return IOUtils.contentEquals(inputStream1, inputStream2);
    552        }
    553    }
    554
    555    /**
    556     * Reads the access control list from a file attribute view.
    557     *
    558     * @param sourcePath the path to the file.
    559     * @return a file attribute view of the specified type, or null ifthe attribute view type is not available.
    560     * @throws IOException if an I/O error occurs.
    561     * @since 2.8.0
    562     */
    563    public static List<AclEntry> getAclEntryList(final Path sourcePath) throws IOException {
    564        final AclFileAttributeView fileAttributeView = Files.getFileAttributeView(sourcePath,
    565            AclFileAttributeView.class);
    566        return fileAttributeView == null ? null : fileAttributeView.getAcl();
    567    }
    568
    569    /**
    570     * Returns whether the given file or directory is empty.
    571     *
    572     * @param path the the given file or directory to query.
    573     * @return whether the given file or directory is empty.
    574     * @throws IOException if an I/O error occurs
    575     */
    576    public static boolean isEmpty(final Path path) throws IOException {
    577        return Files.isDirectory(path) ? isEmptyDirectory(path) : isEmptyFile(path);
    578    }
    579
    580    /**
    581     * Returns whether the directory is empty.
    582     *
    583     * @param directory the the given directory to query.
    584     * @return whether the given directory is empty.
    585     * @throws IOException if an I/O error occurs
    586     */
    587    public static boolean isEmptyDirectory(final Path directory) throws IOException {
    588        try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(directory)) {
    589            if (directoryStream.iterator().hasNext()) {
    590                return false;
    591            }
    592        }
    593        return true;
    594    }
    595
    596    /**
    597     * Returns whether the given file is empty.
    598     *
    599     * @param file the the given file to query.
    600     * @return whether the given file is empty.
    601     * @throws IOException if an I/O error occurs
    602     */
    603    public static boolean isEmptyFile(final Path file) throws IOException {
    604        return Files.size(file) <= 0;
    605    }
    606
    607    /**
    608     * Relativizes all files in the given {@code collection} against a {@code parent}.
    609     *
    610     * @param collection The collection of paths to relativize.
    611     * @param parent relativizes against this parent path.
    612     * @param sort Whether to sort the result.
    613     * @param comparator How to sort.
    614     * @return A collection of relativized paths, optionally sorted.
    615     */
    616    static List<Path> relativize(final Collection<Path> collection, final Path parent, final boolean sort,
    617        final Comparator<? super Path> comparator) {
    618        Stream<Path> stream = collection.stream().map(parent::relativize);
    619        if (sort) {
    620            stream = comparator == null ? stream.sorted() : stream.sorted(comparator);
    621        }
    622        return stream.collect(Collectors.toList());
    623    }
    624
    625    /**
    626     * Sets the given Path to the {@code readOnly} value.
    627     * <p>
    628     * This behavior is OS dependent.
    629     * </p>
    630     *
    631     * @param path The path to set.
    632     * @param readOnly true for read-only, false for not read-only.
    633     * @param options options indicating how symbolic links are handled.
    634     * @return The given path.
    635     * @throws IOException if an I/O error occurs.
    636     * @since 2.8.0
    637     */
    638    public static Path setReadOnly(final Path path, final boolean readOnly, final LinkOption... options)
    639        throws IOException {
    640        final DosFileAttributeView fileAttributeView = Files.getFileAttributeView(path, DosFileAttributeView.class,
    641            options);
    642        if (fileAttributeView != null) {
    643            fileAttributeView.setReadOnly(readOnly);
    644            return path;
    645        }
    646        final PosixFileAttributeView posixFileAttributeView = Files.getFileAttributeView(path,
    647            PosixFileAttributeView.class, options);
    648        if (posixFileAttributeView != null) {
    649            // Works on Windows but not on Ubuntu:
    650            // Files.setAttribute(path, "unix:readonly", readOnly, options);
    651            // java.lang.IllegalArgumentException: 'unix:readonly' not recognized
    652            final PosixFileAttributes readAttributes = posixFileAttributeView.readAttributes();
    653            final Set<PosixFilePermission> permissions = readAttributes.permissions();
    654            permissions.remove(PosixFilePermission.OWNER_WRITE);
    655            permissions.remove(PosixFilePermission.GROUP_WRITE);
    656            permissions.remove(PosixFilePermission.OTHERS_WRITE);
    657            return Files.setPosixFilePermissions(path, permissions);
    658        }
    659        throw new IOException("No DosFileAttributeView or PosixFileAttributeView for " + path);
    660    }
    661
    662    /**
    663     * Converts an array of {@link FileVisitOption} to a {@link Set}.
    664     *
    665     * @param fileVisitOptions input array.
    666     * @return a new Set.
    667     */
    668    static Set<FileVisitOption> toFileVisitOptionSet(final FileVisitOption... fileVisitOptions) {
    669        return fileVisitOptions == null ? EnumSet.noneOf(FileVisitOption.class)
    670            : Arrays.stream(fileVisitOptions).collect(Collectors.toSet());
    671    }
    672
    673    /**
    674     * Performs {@link Files#walkFileTree(Path,FileVisitor)} and returns the given visitor.
    675     *
    676     * Note that {@link Files#walkFileTree(Path,FileVisitor)} returns the given path.
    677     *
    678     * @param visitor See {@link Files#walkFileTree(Path,FileVisitor)}.
    679     * @param directory See {@link Files#walkFileTree(Path,FileVisitor)}.
    680     * @param <T> See {@link Files#walkFileTree(Path,FileVisitor)}.
    681     * @return the given visitor.
    682     *
    683     * @throws IOException if an I/O error is thrown by a visitor method
    684     */
    685    public static <T extends FileVisitor<? super Path>> T visitFileTree(final T visitor, final Path directory)
    686        throws IOException {
    687        Files.walkFileTree(directory, visitor);
    688        return visitor;
    689    }
    690
    691    /**
    692     * Performs {@link Files#walkFileTree(Path,FileVisitor)} and returns the given visitor.
    693     *
    694     * Note that {@link Files#walkFileTree(Path,FileVisitor)} returns the given path.
    695     *
    696     * @param start See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
    697     * @param options See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
    698     * @param maxDepth See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
    699     * @param visitor See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
    700     * @param <T> See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
    701     * @return the given visitor.
    702     *
    703     * @throws IOException if an I/O error is thrown by a visitor method
    704     */
    705    public static <T extends FileVisitor<? super Path>> T visitFileTree(final T visitor, final Path start,
    706        final Set<FileVisitOption> options, final int maxDepth) throws IOException {
    707        Files.walkFileTree(start, options, maxDepth, visitor);
    708        return visitor;
    709    }
    710
    711    /**
    712     * Performs {@link Files#walkFileTree(Path,FileVisitor)} and returns the given visitor.
    713     *
    714     * Note that {@link Files#walkFileTree(Path,FileVisitor)} returns the given path.
    715     *
    716     * @param visitor See {@link Files#walkFileTree(Path,FileVisitor)}.
    717     * @param first See {@link Paths#get(String,String[])}.
    718     * @param more See {@link Paths#get(String,String[])}.
    719     * @param <T> See {@link Files#walkFileTree(Path,FileVisitor)}.
    720     * @return the given visitor.
    721     *
    722     * @throws IOException if an I/O error is thrown by a visitor method
    723     */
    724    public static <T extends FileVisitor<? super Path>> T visitFileTree(final T visitor, final String first,
    725        final String... more) throws IOException {
    726        return visitFileTree(visitor, Paths.get(first, more));
    727    }
    728
    729    /**
    730     * Performs {@link Files#walkFileTree(Path,FileVisitor)} and returns the given visitor.
    731     *
    732     * Note that {@link Files#walkFileTree(Path,FileVisitor)} returns the given path.
    733     *
    734     * @param visitor See {@link Files#walkFileTree(Path,FileVisitor)}.
    735     * @param uri See {@link Paths#get(URI)}.
    736     * @param <T> See {@link Files#walkFileTree(Path,FileVisitor)}.
    737     * @return the given visitor.
    738     *
    739     * @throws IOException if an I/O error is thrown by a visitor method
    740     */
    741    public static <T extends FileVisitor<? super Path>> T visitFileTree(final T visitor, final URI uri)
    742        throws IOException {
    743        return visitFileTree(visitor, Paths.get(uri));
    744    }
    745
    746    /**
    747     * Does allow to instantiate.
    748     */
    749    private PathUtils() {
    750        // do not instantiate.
    751    }
    752
    753}