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.NotDirectoryException;
031import java.nio.file.OpenOption;
032import java.nio.file.Path;
033import java.nio.file.Paths;
034import java.util.Arrays;
035import java.util.Collection;
036import java.util.Collections;
037import java.util.Comparator;
038import java.util.EnumSet;
039import java.util.List;
040import java.util.Set;
041import java.util.stream.Collectors;
042import java.util.stream.Stream;
043
044import org.apache.commons.io.IOUtils;
045import org.apache.commons.io.file.Counters.PathCounters;
046
047/**
048 * NIO Path utilities.
049 *
050 * @since 2.7
051 */
052public final class PathUtils {
053
054    /**
055     * Accumulates file tree information in a {@link AccumulatorPathVisitor}.
056     * 
057     * @param directory The directory to accumulate information.
058     * @param maxDepth See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
059     * @param linkOptions Options indicating how symbolic links are handled.
060     * @param fileVisitOptions See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
061     * @throws IOException if an I/O error is thrown by a visitor method.
062     * @return file tree information.
063     */
064    private static AccumulatorPathVisitor accumulate(final Path directory, final int maxDepth,
065            final LinkOption[] linkOptions, final FileVisitOption[] fileVisitOptions) throws IOException {
066        return visitFileTree(AccumulatorPathVisitor.withLongCounters(), directory,
067                toFileVisitOptionSet(fileVisitOptions), maxDepth);
068    }
069
070    /**
071     * Private worker/holder that computes and tracks relative path names and their equality. We reuse the sorted
072     * relative lists when comparing directories.
073     */
074    private static class RelativeSortedPaths {
075
076        final boolean equals;
077        final List<Path> relativeDirList1; // might need later?
078        final List<Path> relativeDirList2; // might need later?
079        final List<Path> relativeFileList1;
080        final List<Path> relativeFileList2;
081
082        /**
083         * Constructs and initializes a new instance by accumulating directory and file info.
084         * 
085         * @param dir1 First directory to compare.
086         * @param dir2 Seconds directory to compare.
087         * @param maxDepth See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
088         * @param linkOptions Options indicating how symbolic links are handled.
089         * @param fileVisitOptions See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
090         * @throws IOException if an I/O error is thrown by a visitor method.
091         */
092        private RelativeSortedPaths(final Path dir1, final Path dir2, final int maxDepth,
093                final LinkOption[] linkOptions, final FileVisitOption[] fileVisitOptions) throws IOException {
094            List<Path> tmpRelativeDirList1 = null;
095            List<Path> tmpRelativeDirList2 = null;
096            List<Path> tmpRelativeFileList1 = null;
097            List<Path> tmpRelativeFileList2 = null;
098            if (dir1 == null && dir2 == null) {
099                equals = true;
100            } else if (dir1 == null ^ dir2 == null) {
101                equals = false;
102            } else {
103                final boolean parentDirExists1 = Files.exists(dir1, linkOptions);
104                final boolean parentDirExists2 = Files.exists(dir2, linkOptions);
105                if (!parentDirExists1 || !parentDirExists2) {
106                    equals = !parentDirExists1 && !parentDirExists2;
107                } else {
108                    AccumulatorPathVisitor visitor1 = accumulate(dir1, maxDepth, linkOptions, fileVisitOptions);
109                    AccumulatorPathVisitor visitor2 = accumulate(dir2, maxDepth, linkOptions, fileVisitOptions);
110                    if (visitor1.getDirList().size() != visitor2.getDirList().size()
111                            || visitor1.getFileList().size() != visitor2.getFileList().size()) {
112                        equals = false;
113                    } else {
114                        tmpRelativeDirList1 = visitor1.relativizeDirectories(dir1, true, null);
115                        tmpRelativeDirList2 = visitor2.relativizeDirectories(dir2, true, null);
116                        if (!tmpRelativeDirList1.equals(tmpRelativeDirList2)) {
117                            equals = false;
118                        } else {
119                            tmpRelativeFileList1 = visitor1.relativizeFiles(dir1, true, null);
120                            tmpRelativeFileList2 = visitor2.relativizeFiles(dir2, true, null);
121                            equals = tmpRelativeFileList1.equals(tmpRelativeFileList2);
122                        }
123                    }
124                }
125            }
126            relativeDirList1 = tmpRelativeDirList1;
127            relativeDirList2 = tmpRelativeDirList2;
128            relativeFileList1 = tmpRelativeFileList1;
129            relativeFileList2 = tmpRelativeFileList2;
130        }
131    }
132
133    /**
134     * Empty {@link FileVisitOption} array.
135     */
136    public static final FileVisitOption[] EMPTY_FILE_VISIT_OPTION_ARRAY = new FileVisitOption[0];
137
138    /**
139     * Empty {@link LinkOption} array.
140     */
141    public static final LinkOption[] EMPTY_LINK_OPTION_ARRAY = new LinkOption[0];
142
143    /**
144     * Empty {@link OpenOption} array.
145     */
146    public static final OpenOption[] EMPTY_OPEN_OPTION_ARRAY = new OpenOption[0];
147
148    /**
149     * Cleans a directory including sub-directories without deleting directories.
150     *
151     * @param directory directory to clean.
152     * @return The visitation path counters.
153     * @throws IOException if an I/O error is thrown by a visitor method.
154     */
155    public static PathCounters cleanDirectory(final Path directory) throws IOException {
156        return visitFileTree(CleaningPathVisitor.withLongCounters(), directory).getPathCounters();
157    }
158
159    /**
160     * Copies a directory to another directory.
161     *
162     * @param sourceDirectory The source directory.
163     * @param targetDirectory The target directory.
164     * @param copyOptions Specifies how the copying should be done.
165     * @return The visitation path counters.
166     * @throws IOException if an I/O error is thrown by a visitor method.
167     */
168    public static PathCounters copyDirectory(final Path sourceDirectory, final Path targetDirectory,
169            final CopyOption... copyOptions) throws IOException {
170        return visitFileTree(
171                new CopyDirectoryVisitor(Counters.longPathCounters(), sourceDirectory, targetDirectory, copyOptions),
172                sourceDirectory).getPathCounters();
173    }
174
175    /**
176     * Copies a URL to a directory.
177     *
178     * @param sourceFile The source URL.
179     * @param targetFile The target file.
180     * @param copyOptions Specifies how the copying should be done.
181     * @return The target file
182     * @throws IOException if an I/O error occurs
183     * @see Files#copy(InputStream, Path, CopyOption...)
184     */
185    public static Path copyFile(final URL sourceFile, final Path targetFile, final CopyOption... copyOptions)
186            throws IOException {
187        try (final InputStream inputStream = sourceFile.openStream()) {
188            Files.copy(inputStream, targetFile, copyOptions);
189            return targetFile;
190        }
191    }
192
193    /**
194     * Copies a file to a directory.
195     *
196     * @param sourceFile The source file.
197     * @param targetDirectory The target directory.
198     * @param copyOptions Specifies how the copying should be done.
199     * @return The target file
200     * @throws IOException if an I/O error occurs
201     * @see Files#copy(Path, Path, CopyOption...)
202     */
203    public static Path copyFileToDirectory(final Path sourceFile, final Path targetDirectory,
204            final CopyOption... copyOptions) throws IOException {
205        return Files.copy(sourceFile, targetDirectory.resolve(sourceFile.getFileName()), copyOptions);
206    }
207
208    /**
209     * Copies a URL to a directory.
210     *
211     * @param sourceFile The source URL.
212     * @param targetDirectory The target directory.
213     * @param copyOptions Specifies how the copying should be done.
214     * @return The target file
215     * @throws IOException if an I/O error occurs
216     * @see Files#copy(InputStream, Path, CopyOption...)
217     */
218    public static Path copyFileToDirectory(final URL sourceFile, final Path targetDirectory,
219            final CopyOption... copyOptions) throws IOException {
220        try (final InputStream inputStream = sourceFile.openStream()) {
221            Files.copy(inputStream, targetDirectory.resolve(sourceFile.getFile()), copyOptions);
222            return targetDirectory;
223        }
224    }
225
226    /**
227     * Counts aspects of a directory including sub-directories.
228     *
229     * @param directory directory to delete.
230     * @return The visitor used to count the given directory.
231     * @throws IOException if an I/O error is thrown by a visitor method.
232     */
233    public static PathCounters countDirectory(final Path directory) throws IOException {
234        return visitFileTree(new CountingPathVisitor(Counters.longPathCounters()), directory).getPathCounters();
235    }
236
237    /**
238     * Deletes a file or directory. If the path is a directory, delete it and all sub-directories.
239     * <p>
240     * The difference between File.delete() and this method are:
241     * </p>
242     * <ul>
243     * <li>A directory to delete does not have to be empty.</li>
244     * <li>You get exceptions when a file or directory cannot be deleted; {@link java.io.File#delete()} returns a
245     * boolean.
246     * </ul>
247     *
248     * @param path file or directory to delete, must not be {@code null}
249     * @return The visitor used to delete the given directory.
250     * @throws NullPointerException if the directory is {@code null}
251     * @throws IOException if an I/O error is thrown by a visitor method or if an I/O error occurs.
252     */
253    public static PathCounters delete(final Path path) throws IOException {
254        return Files.isDirectory(path) ? deleteDirectory(path) : deleteFile(path);
255    }
256
257    /**
258     * Deletes a directory including sub-directories.
259     *
260     * @param directory directory to delete.
261     * @return The visitor used to delete the given directory.
262     * @throws IOException if an I/O error is thrown by a visitor method.
263     */
264    public static PathCounters deleteDirectory(final Path directory) throws IOException {
265        return visitFileTree(DeletingPathVisitor.withLongCounters(), directory).getPathCounters();
266    }
267
268    /**
269     * Deletes the given file.
270     *
271     * @param file The file to delete.
272     * @return A visitor with path counts set to 1 file, 0 directories, and the size of the deleted file.
273     * @throws IOException if an I/O error occurs.
274     * @throws NotDirectoryException if the file is a directory.
275     */
276    public static PathCounters deleteFile(final Path file) throws IOException {
277        if (Files.isDirectory(file)) {
278            throw new NotDirectoryException(file.toString());
279        }
280        final PathCounters pathCounts = Counters.longPathCounters();
281        final long size = Files.exists(file) ? Files.size(file) : 0;
282        if (Files.deleteIfExists(file)) {
283            pathCounts.getFileCounter().increment();
284            pathCounts.getByteCounter().add(size);
285        }
286        return pathCounts;
287    }
288
289    /**
290     * Compares the file sets of two Paths to determine if they are equal or not while considering file contents. The
291     * comparison includes all files in all sub-directories.
292     * 
293     * @param path1 The first directory.
294     * @param path2 The second directory.
295     * @return Whether the two directories contain the same files while considering file contents.
296     * @throws IOException if an I/O error is thrown by a visitor method
297     */
298    public static boolean directoryAndFileContentEquals(final Path path1, final Path path2) throws IOException {
299        return directoryAndFileContentEquals(path1, path2, EMPTY_LINK_OPTION_ARRAY, EMPTY_OPEN_OPTION_ARRAY,
300                EMPTY_FILE_VISIT_OPTION_ARRAY);
301    }
302
303    /**
304     * Compares the file sets of two Paths to determine if they are equal or not while considering file contents. The
305     * comparison includes all files in all sub-directories.
306     * 
307     * @param path1 The first directory.
308     * @param path2 The second directory.
309     * @param linkOptions options to follow links.
310     * @param openOptions options to open files.
311     * @param fileVisitOption options to configure traversal.
312     * @return Whether the two directories contain the same files while considering file contents.
313     * @throws IOException if an I/O error is thrown by a visitor method
314     */
315    public static boolean directoryAndFileContentEquals(final Path path1, final Path path2,
316            final LinkOption[] linkOptions, final OpenOption[] openOptions, final FileVisitOption[] fileVisitOption)
317            throws IOException {
318        // First walk both file trees and gather normalized paths.
319        if (path1 == null && path2 == null) {
320            return true;
321        }
322        if (path1 == null ^ path2 == null) {
323            return false;
324        }
325        if (!Files.exists(path1) && !Files.exists(path2)) {
326            return true;
327        }
328        final RelativeSortedPaths relativeSortedPaths = new RelativeSortedPaths(path1, path2, Integer.MAX_VALUE,
329                linkOptions, fileVisitOption);
330        // If the normalized path names and counts are not the same, no need to compare contents.
331        if (!relativeSortedPaths.equals) {
332            return false;
333        }
334        // Both visitors contain the same normalized paths, we can compare file contents.
335        final List<Path> fileList1 = relativeSortedPaths.relativeFileList1;
336        final List<Path> fileList2 = relativeSortedPaths.relativeFileList2;
337        for (Path path : fileList1) {
338            final int binarySearch = Collections.binarySearch(fileList2, path);
339            if (binarySearch > -1) {
340                if (!fileContentEquals(path1.resolve(path), path2.resolve(path), linkOptions, openOptions)) {
341                    return false;
342                }
343            } else {
344                throw new IllegalStateException(String.format("Unexpected mismatch."));
345            }
346        }
347        return true;
348    }
349
350    /**
351     * Compares the file sets of two Paths to determine if they are equal or not without considering file contents. The
352     * comparison includes all files in all sub-directories.
353     * 
354     * @param path1 The first directory.
355     * @param path2 The second directory.
356     * @return Whether the two directories contain the same files without considering file contents.
357     * @throws IOException if an I/O error is thrown by a visitor method
358     */
359    public static boolean directoryContentEquals(final Path path1, final Path path2) throws IOException {
360        return directoryContentEquals(path1, path2, Integer.MAX_VALUE, EMPTY_LINK_OPTION_ARRAY,
361                EMPTY_FILE_VISIT_OPTION_ARRAY);
362    }
363
364    /**
365     * Compares the file sets of two Paths to determine if they are equal or not without considering file contents. The
366     * comparison includes all files in all sub-directories.
367     * 
368     * @param path1 The first directory.
369     * @param path2 The second directory.
370     * @param maxDepth See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
371     * @param linkOptions options to follow links.
372     * @param fileVisitOptions options to configure the traversal
373     * @return Whether the two directories contain the same files without considering file contents.
374     * @throws IOException if an I/O error is thrown by a visitor method
375     */
376    public static boolean directoryContentEquals(final Path path1, final Path path2, final int maxDepth,
377            LinkOption[] linkOptions, FileVisitOption[] fileVisitOptions) throws IOException {
378        return new RelativeSortedPaths(path1, path2, maxDepth, linkOptions, fileVisitOptions).equals;
379    }
380
381    /**
382     * Compares the file contents of two Paths to determine if they are equal or not.
383     * <p>
384     * File content is accessed through {@link Files#newInputStream(Path,OpenOption...)}.
385     * </p>
386     *
387     * @param path1 the first stream.
388     * @param path2 the second stream.
389     * @return true if the content of the streams are equal or they both don't exist, false otherwise.
390     * @throws NullPointerException if either input is null.
391     * @throws IOException if an I/O error occurs.
392     * @see org.apache.commons.io.FileUtils#contentEquals(java.io.File, java.io.File)
393     */
394    public static boolean fileContentEquals(final Path path1, final Path path2) throws IOException {
395        return fileContentEquals(path1, path2, EMPTY_LINK_OPTION_ARRAY, EMPTY_OPEN_OPTION_ARRAY);
396    }
397
398    /**
399     * Compares the file contents of two Paths to determine if they are equal or not.
400     * <p>
401     * File content is accessed through {@link Files#newInputStream(Path,OpenOption...)}.
402     * </p>
403     *
404     * @param path1 the first stream.
405     * @param path2 the second stream.
406     * @param linkOptions options specifying how files are followed.
407     * @param openOptions options specifying how files are opened.
408     * @return true if the content of the streams are equal or they both don't exist, false otherwise.
409     * @throws NullPointerException if either input is null.
410     * @throws IOException if an I/O error occurs.
411     * @see org.apache.commons.io.FileUtils#contentEquals(java.io.File, java.io.File)
412     */
413    public static boolean fileContentEquals(final Path path1, final Path path2, final LinkOption[] linkOptions,
414            final OpenOption[] openOptions) throws IOException {
415        if (path1 == null && path2 == null) {
416            return true;
417        }
418        if (path1 == null ^ path2 == null) {
419            return false;
420        }
421        final Path nPath1 = path1.normalize();
422        final Path nPath2 = path2.normalize();
423        final boolean path1Exists = Files.exists(nPath1, linkOptions);
424        if (path1Exists != Files.exists(nPath2, linkOptions)) {
425            return false;
426        }
427        if (!path1Exists) {
428            // Two not existing files are equal?
429            // Same as FileUtils
430            return true;
431        }
432        if (Files.isDirectory(nPath1, linkOptions)) {
433            // don't compare directory contents.
434            throw new IOException("Can't compare directories, only files: " + nPath1);
435        }
436        if (Files.isDirectory(nPath2, linkOptions)) {
437            // don't compare directory contents.
438            throw new IOException("Can't compare directories, only files: " + nPath2);
439        }
440        if (Files.size(nPath1) != Files.size(nPath2)) {
441            // lengths differ, cannot be equal
442            return false;
443        }
444        if (path1.equals(path2)) {
445            // same file
446            return true;
447        }
448        try (final InputStream inputStream1 = Files.newInputStream(nPath1, openOptions);
449                final InputStream inputStream2 = Files.newInputStream(nPath2, openOptions)) {
450            return IOUtils.contentEquals(inputStream1, inputStream2);
451        }
452    }
453
454    /**
455     * Returns whether the given file or directory is empty.
456     *
457     * @param path the the given file or directory to query.
458     * @return whether the given file or directory is empty.
459     * @throws IOException if an I/O error occurs
460     */
461    public static boolean isEmpty(final Path path) throws IOException {
462        return Files.isDirectory(path) ? isEmptyDirectory(path) : isEmptyFile(path);
463    }
464
465    /**
466     * Returns whether the directory is empty.
467     *
468     * @param directory the the given directory to query.
469     * @return whether the given directory is empty.
470     * @throws IOException if an I/O error occurs
471     */
472    public static boolean isEmptyDirectory(final Path directory) throws IOException {
473        try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(directory)) {
474            if (directoryStream.iterator().hasNext()) {
475                return false;
476            }
477        }
478        return true;
479    }
480
481    /**
482     * Returns whether the given file is empty.
483     *
484     * @param file the the given file to query.
485     * @return whether the given file is empty.
486     * @throws IOException if an I/O error occurs
487     */
488    public static boolean isEmptyFile(final Path file) throws IOException {
489        return Files.size(file) <= 0;
490    }
491
492    /**
493     * Relativizes all files in the given {@code collection} against a {@code parent}.
494     * 
495     * @param collection The collection of paths to relativize.
496     * @param parent relativizes against this parent path.
497     * @param sort Whether to sort the result.
498     * @param comparator How to sort.
499     * @return A collection of relativized paths, optionally sorted.
500     */
501    static List<Path> relativize(Collection<Path> collection, Path parent, boolean sort,
502            Comparator<? super Path> comparator) {
503        Stream<Path> stream = collection.stream().map(e -> parent.relativize(e));
504        if (sort) {
505            stream = comparator == null ? stream.sorted() : stream.sorted(comparator);
506        }
507        return stream.collect(Collectors.toList());
508    }
509
510    /**
511     * Converts an array of {@link FileVisitOption} to a {@link Set}.
512     * 
513     * @param fileVisitOptions input array.
514     * @return a new Set.
515     */
516    static Set<FileVisitOption> toFileVisitOptionSet(final FileVisitOption... fileVisitOptions) {
517        return fileVisitOptions == null ? EnumSet.noneOf(FileVisitOption.class)
518                : Arrays.stream(fileVisitOptions).collect(Collectors.toSet());
519    }
520
521    /**
522     * Performs {@link Files#walkFileTree(Path,FileVisitor)} and returns the given visitor.
523     *
524     * Note that {@link Files#walkFileTree(Path,FileVisitor)} returns the given path.
525     *
526     * @param visitor See {@link Files#walkFileTree(Path,FileVisitor)}.
527     * @param directory See {@link Files#walkFileTree(Path,FileVisitor)}.
528     * @param <T> See {@link Files#walkFileTree(Path,FileVisitor)}.
529     * @return the given visitor.
530     *
531     * @throws IOException if an I/O error is thrown by a visitor method
532     */
533    public static <T extends FileVisitor<? super Path>> T visitFileTree(final T visitor, final Path directory)
534            throws IOException {
535        Files.walkFileTree(directory, visitor);
536        return visitor;
537    }
538
539    /**
540     * Performs {@link Files#walkFileTree(Path,FileVisitor)} and returns the given visitor.
541     *
542     * Note that {@link Files#walkFileTree(Path,FileVisitor)} returns the given path.
543     *
544     * @param start See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
545     * @param options See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
546     * @param maxDepth See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
547     * @param visitor See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
548     * @param <T> See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
549     * @return the given visitor.
550     *
551     * @throws IOException if an I/O error is thrown by a visitor method
552     */
553    public static <T extends FileVisitor<? super Path>> T visitFileTree(T visitor, Path start,
554            Set<FileVisitOption> options, int maxDepth) throws IOException {
555        Files.walkFileTree(start, options, maxDepth, visitor);
556        return visitor;
557    }
558
559    /**
560     * Performs {@link Files#walkFileTree(Path,FileVisitor)} and returns the given visitor.
561     *
562     * Note that {@link Files#walkFileTree(Path,FileVisitor)} returns the given path.
563     *
564     * @param visitor See {@link Files#walkFileTree(Path,FileVisitor)}.
565     * @param first See {@link Paths#get(String,String[])}.
566     * @param more See {@link Paths#get(String,String[])}.
567     * @param <T> See {@link Files#walkFileTree(Path,FileVisitor)}.
568     * @return the given visitor.
569     *
570     * @throws IOException if an I/O error is thrown by a visitor method
571     */
572    public static <T extends FileVisitor<? super Path>> T visitFileTree(final T visitor, final String first,
573            final String... more) throws IOException {
574        return visitFileTree(visitor, Paths.get(first, more));
575    }
576
577    /**
578     * Performs {@link Files#walkFileTree(Path,FileVisitor)} and returns the given visitor.
579     *
580     * Note that {@link Files#walkFileTree(Path,FileVisitor)} returns the given path.
581     *
582     * @param visitor See {@link Files#walkFileTree(Path,FileVisitor)}.
583     * @param uri See {@link Paths#get(URI)}.
584     * @param <T> See {@link Files#walkFileTree(Path,FileVisitor)}.
585     * @return the given visitor.
586     *
587     * @throws IOException if an I/O error is thrown by a visitor method
588     */
589    public static <T extends FileVisitor<? super Path>> T visitFileTree(final T visitor, final URI uri)
590            throws IOException {
591        return visitFileTree(visitor, Paths.get(uri));
592    }
593
594    /**
595     * Does allow to instantiate.
596     */
597    private PathUtils() {
598        // do not instantiate.
599    }
600
601}