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}