This is an automated email from the ASF dual-hosted git repository.

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-io.git


The following commit(s) were added to refs/heads/master by this push:
     new 4d02b11  Implement directory content equality. (#100)
4d02b11 is described below

commit 4d02b112100d7dfcd21b2ad6d0ed8947fd7ffe95
Author: Gary Gregory <garydgreg...@users.noreply.github.com>
AuthorDate: Wed Dec 25 10:37:28 2019 -0500

    Implement directory content equality. (#100)
---
 pom.xml                                            |   1 +
 src/main/java/org/apache/commons/io/FileUtils.java |   2 +-
 .../commons/io/file/AccumulatorPathVisitor.java    | 146 ++++++++
 .../java/org/apache/commons/io/file/PathUtils.java | 410 +++++++++++++++++----
 .../io/file/PathUtilsContentEqualsTest.java        | 112 ++++++
 .../directory-files-only1/file1.txt                |   1 +
 .../directory-files-only1/file2.txt                |   1 +
 .../dirs-and-files1/file1.txt                      |   1 +
 .../dirs-and-files1/file2.txt                      |   1 +
 .../directory-files-only2/file1.txt                |   1 +
 .../directory-files-only2/file2.txt                |   1 +
 .../dirs-and-files2/file1.txt                      |   1 +
 .../dirs-and-files2/file2.txt                      |   1 +
 .../dir1/directory-files-only1/file1.txt           |   1 +
 .../dir1/directory-files-only1/file2.txt           |   1 +
 .../dir2/directory-files-only1/file1.txt           |   1 +
 .../dir2/directory-files-only1/file2.txt           |   1 +
 .../directory-files-only1/file1.txt                |   1 +
 .../directory-files-only1/file2.txt                |   1 +
 .../directory-files-only2/file1.txt                |   1 +
 .../directory-files-only2/file2.txt                |   1 +
 .../directory-files-only1/file1.txt                |   1 +
 .../directory-files-only1/file2.txt                |   1 +
 .../directory-files-only2/file1.txt                |   1 +
 .../directory-files-only2/file2.txt                |   1 +
 25 files changed, 617 insertions(+), 74 deletions(-)

diff --git a/pom.xml b/pom.xml
index 8701476..443a293 100644
--- a/pom.xml
+++ b/pom.xml
@@ -304,6 +304,7 @@ file comparators, endian transformation classes, and much 
more.
           <configuration>
             <excludes>
               <exclude>src/test/resources/**/*.bin</exclude>
+              <exclude>src/test/resources/dir-equals-tests/**</exclude>
               <exclude>test/**</exclude>
             </excludes>
           </configuration>
diff --git a/src/main/java/org/apache/commons/io/FileUtils.java 
b/src/main/java/org/apache/commons/io/FileUtils.java
index 6e5233a..7f617b6 100644
--- a/src/main/java/org/apache/commons/io/FileUtils.java
+++ b/src/main/java/org/apache/commons/io/FileUtils.java
@@ -386,7 +386,7 @@ public class FileUtils {
      * @return true if the content of the files are equal or they both don't
      * exist, false otherwise
      * @throws IOException in case of an I/O error
-     * @see 
org.apache.commons.io.file.PathUtils#fileContentEquals(Path,Path,java.nio.file.OpenOption...)
+     * @see 
org.apache.commons.io.file.PathUtils#fileContentEquals(Path,Path,java.nio.file.LinkOption[],java.nio.file.OpenOption...)
      */
     public static boolean contentEquals(final File file1, final File file2) 
throws IOException {
         if (file1 == null && file2 == null) {
diff --git 
a/src/main/java/org/apache/commons/io/file/AccumulatorPathVisitor.java 
b/src/main/java/org/apache/commons/io/file/AccumulatorPathVisitor.java
new file mode 100644
index 0000000..8fe7028
--- /dev/null
+++ b/src/main/java/org/apache/commons/io/file/AccumulatorPathVisitor.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.io.file;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.commons.io.file.Counters.PathCounters;
+
+/**
+ * Accumulates normalized paths during visitation.
+ * <p>
+ * Use with care on large file trees as each visited Path element is 
remembered.
+ * </p>
+ *
+ * @since 2.7
+ */
+public class AccumulatorPathVisitor extends CountingPathVisitor {
+
+    /**
+     * Creates a new instance configured with a BigInteger {@link 
PathCounters}.
+     *
+     * @return a new instance configured with a BigInteger {@link 
PathCounters}.
+     */
+    public static AccumulatorPathVisitor withBigIntegerCounters() {
+        return new AccumulatorPathVisitor(Counters.bigIntegerPathCounters());
+    }
+
+    /**
+     * Creates a new instance configured with a long {@link PathCounters}.
+     *
+     * @return a new instance configured with a long {@link PathCounters}.
+     */
+    public static AccumulatorPathVisitor withLongCounters() {
+        return new AccumulatorPathVisitor(Counters.longPathCounters());
+    }
+
+    private final List<Path> dirList = new ArrayList<>();
+
+    private final List<Path> fileList = new ArrayList<>();
+
+    /**
+     * Constructs a new instance.
+     *
+     * @param pathCounter How to count path visits.
+     */
+    public AccumulatorPathVisitor(PathCounters pathCounter) {
+        super(pathCounter);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!super.equals(obj)) {
+            return false;
+        }
+        if (!(obj instanceof AccumulatorPathVisitor)) {
+            return false;
+        }
+        AccumulatorPathVisitor other = (AccumulatorPathVisitor) obj;
+        return Objects.equals(dirList, other.dirList) && 
Objects.equals(fileList, other.fileList);
+    }
+
+    /**
+     * Gets the list of visited directories.
+     * 
+     * @return the list of visited directories.
+     */
+    public List<Path> getDirList() {
+        return dirList;
+    }
+
+    /**
+     * Gets the list of visited files.
+     * 
+     * @return the list of visited files.
+     */
+    public List<Path> getFileList() {
+        return fileList;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = super.hashCode();
+        result = prime * result + Objects.hash(dirList, fileList);
+        return result;
+    }
+
+    /**
+     * Relativizes each directory path with {@link Path#relativize(Path)} 
against the given {@code parent}, optionally
+     * sorting the result.
+     * 
+     * @param parent A parent path
+     * @param sort Whether to sort
+     * @param comparator How to sort, null uses default sorting.
+     * @return A new list
+     */
+    public List<Path> relativizeDirectories(final Path parent, boolean sort, 
Comparator<? super Path> comparator) {
+        return PathUtils.relativize(getDirList(), parent, sort, comparator);
+    }
+
+    /**
+     * Relativizes each file path with {@link Path#relativize(Path)} against 
the given {@code parent}, optionally
+     * sorting the result.
+     * 
+     * @param parent A parent path
+     * @param sort Whether to sort
+     * @param comparator How to sort, null uses default sorting.
+     * @return A new list
+     */
+    public List<Path> relativizeFiles(final Path parent, boolean sort, 
Comparator<? super Path> comparator) {
+        return PathUtils.relativize(getFileList(), parent, sort, comparator);
+    }
+
+    @Override
+    public FileVisitResult visitFile(Path file, BasicFileAttributes 
attributes) throws IOException {
+        ((Files.isDirectory(file)) ? dirList : fileList).add(file.normalize());
+        return super.visitFile(file, attributes);
+    }
+
+}
diff --git a/src/main/java/org/apache/commons/io/file/PathUtils.java 
b/src/main/java/org/apache/commons/io/file/PathUtils.java
index 5f3d0e0..317fd99 100644
--- a/src/main/java/org/apache/commons/io/file/PathUtils.java
+++ b/src/main/java/org/apache/commons/io/file/PathUtils.java
@@ -23,12 +23,23 @@ import java.net.URI;
 import java.net.URL;
 import java.nio.file.CopyOption;
 import java.nio.file.DirectoryStream;
+import java.nio.file.FileVisitOption;
 import java.nio.file.FileVisitor;
 import java.nio.file.Files;
+import java.nio.file.LinkOption;
 import java.nio.file.NotDirectoryException;
 import java.nio.file.OpenOption;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.io.file.Counters.PathCounters;
@@ -41,6 +52,100 @@ import org.apache.commons.io.file.Counters.PathCounters;
 public final class PathUtils {
 
     /**
+     * Accumulates file tree information in a {@link AccumulatorPathVisitor}.
+     * 
+     * @param directory The directory to accumulate information.
+     * @param maxDepth See {@link 
Files#walkFileTree(Path,Set,int,FileVisitor)}.
+     * @param linkOptions Options indicating how symbolic links are handled.
+     * @param fileVisitOptions See {@link 
Files#walkFileTree(Path,Set,int,FileVisitor)}.
+     * @throws IOException if an I/O error is thrown by a visitor method.
+     * @return file tree information.
+     */
+    private static AccumulatorPathVisitor accumulate(final Path directory, 
final int maxDepth,
+            final LinkOption[] linkOptions, final FileVisitOption... 
fileVisitOptions) throws IOException {
+        return visitFileTree(AccumulatorPathVisitor.withLongCounters(), 
directory,
+                toFileVisitOptionSet(fileVisitOptions), maxDepth);
+    }
+
+    /**
+     * Private worker/holder that computes and tracks relative path names and 
their equality. We reuse the sorted
+     * relative lists when comparing directories.
+     */
+    private static class RelativeSortedPaths {
+
+        final boolean equals;
+        final List<Path> relativeDirList1; // might need later?
+        final List<Path> relativeDirList2; // might need later?
+        final List<Path> relativeFileList1;
+        final List<Path> relativeFileList2;
+
+        /**
+         * Constructs and initializes a new instance by accumulating directory 
and file info.
+         * 
+         * @param dir1 First directory to compare.
+         * @param dir2 Seconds directory to compare.
+         * @param maxDepth See {@link 
Files#walkFileTree(Path,Set,int,FileVisitor)}.
+         * @param linkOptions Options indicating how symbolic links are 
handled.
+         * @param fileVisitOptions See {@link 
Files#walkFileTree(Path,Set,int,FileVisitor)}.
+         * @throws IOException if an I/O error is thrown by a visitor method.
+         */
+        private RelativeSortedPaths(final Path dir1, final Path dir2, final 
int maxDepth,
+                final LinkOption[] linkOptions, final FileVisitOption... 
fileVisitOptions) throws IOException {
+            List<Path> tmpRelativeDirList1 = null;
+            List<Path> tmpRelativeDirList2 = null;
+            List<Path> tmpRelativeFileList1 = null;
+            List<Path> tmpRelativeFileList2 = null;
+            if (dir1 == null && dir2 == null) {
+                equals = true;
+            } else if (dir1 == null ^ dir2 == null) {
+                equals = false;
+            } else {
+                final boolean parentDirExists1 = Files.exists(dir1, 
linkOptions);
+                final boolean parentDirExists2 = Files.exists(dir2, 
linkOptions);
+                if (!parentDirExists1 || !parentDirExists2) {
+                    equals = !parentDirExists1 && !parentDirExists2;
+                } else {
+                    AccumulatorPathVisitor visitor1 = accumulate(dir1, 
maxDepth, linkOptions, fileVisitOptions);
+                    AccumulatorPathVisitor visitor2 = accumulate(dir2, 
maxDepth, linkOptions, fileVisitOptions);
+                    if (visitor1.getDirList().size() != 
visitor2.getDirList().size()
+                            || visitor1.getFileList().size() != 
visitor2.getFileList().size()) {
+                        equals = false;
+                    } else {
+                        tmpRelativeDirList1 = 
visitor1.relativizeDirectories(dir1, true, null);
+                        tmpRelativeDirList2 = 
visitor2.relativizeDirectories(dir2, true, null);
+                        if (!tmpRelativeDirList1.equals(tmpRelativeDirList2)) {
+                            equals = false;
+                        } else {
+                            tmpRelativeFileList1 = 
visitor1.relativizeFiles(dir1, true, null);
+                            tmpRelativeFileList2 = 
visitor2.relativizeFiles(dir2, true, null);
+                            equals = 
tmpRelativeFileList1.equals(tmpRelativeFileList2);
+                        }
+                    }
+                }
+            }
+            relativeDirList1 = tmpRelativeDirList1;
+            relativeDirList2 = tmpRelativeDirList2;
+            relativeFileList1 = tmpRelativeFileList1;
+            relativeFileList2 = tmpRelativeFileList2;
+        }
+    }
+
+    /**
+     * Empty {@link FileVisitOption} array.
+     */
+    public static final FileVisitOption[] EMPTY_FILE_VISIT_OPTION_ARRAY = new 
FileVisitOption[0];
+
+    /**
+     * Empty {@link LinkOption} array.
+     */
+    public static final LinkOption[] EMPTY_LINK_OPTION_ARRAY = new 
LinkOption[0];
+
+    /**
+     * Empty {@link OpenOption} array.
+     */
+    public static final OpenOption[] EMPTY_OPEN_OPTION_ARRAY = new 
OpenOption[0];
+
+    /**
      * Cleans a directory including sub-directories without deleting 
directories.
      *
      * @param directory directory to clean.
@@ -52,60 +157,6 @@ public final class PathUtils {
     }
 
     /**
-     * Compares the contents of two Paths to determine if they are equal or 
not.
-     * <p>
-     * File content is accessed through {@link 
Files#newInputStream(Path,OpenOption...)}.
-     * </p>
-     *
-     * @param path1 the first stream.
-     * @param path2 the second stream.
-     * @param options options specifying how the files are opened.
-     * @return true if the content of the streams are equal or they both don't 
exist, false otherwise.
-     * @throws NullPointerException if either input is null.
-     * @throws IOException if an I/O error occurs.
-     * @see org.apache.commons.io.FileUtils#contentEquals(java.io.File, 
java.io.File)
-     */
-    public static boolean fileContentEquals(final Path path1, final Path 
path2, final OpenOption... options) throws IOException {
-        if (path1 == null && path2 == null) {
-            return true;
-        }
-        if (path1 == null ^ path2 == null) {
-            return false;
-        }
-        final Path nPath1 = path1.normalize();
-        final Path nPath2 = path2.normalize();
-        final boolean path1Exists = Files.exists(nPath1);
-        if (path1Exists != Files.exists(nPath2)) {
-            return false;
-        }
-        if (!path1Exists) {
-            // Two not existing files are equal?
-            // Same as FileUtils
-            return true;
-        }
-        if (Files.isDirectory(nPath1)) {
-            // don't compare directory contents.
-            throw new IOException("Can't compare directories, only files: " + 
nPath1);
-        }
-        if (Files.isDirectory(nPath2)) {
-            // don't compare directory contents.
-            throw new IOException("Can't compare directories, only files: " + 
nPath2);
-        }
-        if (Files.size(nPath1) != Files.size(nPath2)) {
-            // lengths differ, cannot be equal
-            return false;
-        }
-        if (path1.equals(path2)) {
-            // same file
-            return true;
-        }
-        try (final InputStream inputStream1 = Files.newInputStream(nPath1, 
options);
-                final InputStream inputStream2 = Files.newInputStream(nPath2, 
options)) {
-            return IOUtils.contentEquals(inputStream1, inputStream2);
-        }
-    }
-
-    /**
      * Copies a directory to another directory.
      *
      * @param sourceDirectory The source directory.
@@ -122,6 +173,24 @@ public final class PathUtils {
     }
 
     /**
+     * Copies a URL to a directory.
+     *
+     * @param sourceFile The source URL.
+     * @param targetFile The target file.
+     * @param copyOptions Specifies how the copying should be done.
+     * @return The target file
+     * @throws IOException if an I/O error occurs
+     * @see Files#copy(InputStream, Path, CopyOption...)
+     */
+    public static Path copyFile(final URL sourceFile, final Path targetFile, 
final CopyOption... copyOptions)
+            throws IOException {
+        try (final InputStream inputStream = sourceFile.openStream()) {
+            Files.copy(inputStream, targetFile, copyOptions);
+            return targetFile;
+        }
+    }
+
+    /**
      * Copies a file to a directory.
      *
      * @param sourceFile The source file.
@@ -155,24 +224,6 @@ public final class PathUtils {
     }
 
     /**
-     * Copies a URL to a directory.
-     *
-     * @param sourceFile The source URL.
-     * @param targetFile The target file.
-     * @param copyOptions Specifies how the copying should be done.
-     * @return The target file
-     * @throws IOException if an I/O error occurs
-     * @see Files#copy(InputStream, Path, CopyOption...)
-     */
-    public static Path copyFile(final URL sourceFile, final Path targetFile,
-            final CopyOption... copyOptions) throws IOException {
-        try (final InputStream inputStream = sourceFile.openStream()) {
-            Files.copy(inputStream, targetFile, copyOptions);
-            return targetFile;
-        }
-    }
-
-    /**
      * Counts aspects of a directory including sub-directories.
      *
      * @param directory directory to delete.
@@ -236,6 +287,171 @@ public final class PathUtils {
     }
 
     /**
+     * Compares the file sets of two Paths to determine if they are equal or 
not while considering file contents. The
+     * comparison includes all files in all sub-directories.
+     * 
+     * @param path1 The first directory.
+     * @param path2 The second directory.
+     * @return Whether the two directories contain the same files while 
considering file contents.
+     * @throws IOException if an I/O error is thrown by a visitor method
+     */
+    public static boolean directoryAndFileContentEquals(final Path path1, 
final Path path2) throws IOException {
+        return directoryAndFileContentEquals(path1, path2, 
EMPTY_LINK_OPTION_ARRAY, EMPTY_OPEN_OPTION_ARRAY,
+                EMPTY_FILE_VISIT_OPTION_ARRAY);
+    }
+
+    /**
+     * Compares the file sets of two Paths to determine if they are equal or 
not while considering file contents. The
+     * comparison includes all files in all sub-directories.
+     * 
+     * @param path1 The first directory.
+     * @param path2 The second directory.
+     * @param linkOptions options to follow links.
+     * @param openOptions options to open files.
+     * @param fileVisitOption options to configure traversal.
+     * @return Whether the two directories contain the same files while 
considering file contents.
+     * @throws IOException if an I/O error is thrown by a visitor method
+     */
+    public static boolean directoryAndFileContentEquals(final Path path1, 
final Path path2,
+            final LinkOption[] linkOptions, final OpenOption[] openOptions, 
final FileVisitOption... fileVisitOption)
+            throws IOException {
+        // First walk both file trees and gather normalized paths.
+        if (path1 == null && path2 == null) {
+            return true;
+        }
+        if (path1 == null ^ path2 == null) {
+            return false;
+        }
+        if (!Files.exists(path1) && !Files.exists(path2)) {
+            return true;
+        }
+        final RelativeSortedPaths relativeSortedPaths = new 
RelativeSortedPaths(path1, path2, Integer.MAX_VALUE,
+                linkOptions, fileVisitOption);
+        // If the normalized path names and counts are not the same, no need 
to compare contents.
+        if (!relativeSortedPaths.equals) {
+            return false;
+        }
+        // Both visitors contain the same normalized paths, we can compare 
file contents.
+        final List<Path> fileList1 = relativeSortedPaths.relativeFileList1;
+        final List<Path> fileList2 = relativeSortedPaths.relativeFileList2;
+        for (Path path : fileList1) {
+            final int binarySearch = Collections.binarySearch(fileList2, path);
+            if (binarySearch > -1) {
+                if (!fileContentEquals(path1.resolve(path), 
path2.resolve(path), linkOptions, openOptions)) {
+                    return false;
+                }
+            } else {
+                throw new IllegalStateException(String.format("Unexpected 
mismatch."));
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Compares the file sets of two Paths to determine if they are equal or 
not without considering file contents. The
+     * comparison includes all files in all sub-directories.
+     * 
+     * @param path1 The first directory.
+     * @param path2 The second directory.
+     * @return Whether the two directories contain the same files without 
considering file contents.
+     * @throws IOException if an I/O error is thrown by a visitor method
+     */
+    public static boolean directoryContentEquals(final Path path1, final Path 
path2) throws IOException {
+        return directoryContentEquals(path1, path2, Integer.MAX_VALUE, 
EMPTY_LINK_OPTION_ARRAY,
+                EMPTY_FILE_VISIT_OPTION_ARRAY);
+    }
+
+    /**
+     * Compares the file sets of two Paths to determine if they are equal or 
not without considering file contents. The
+     * comparison includes all files in all sub-directories.
+     * 
+     * @param path1 The first directory.
+     * @param path2 The second directory.
+     * @param maxDepth See {@link 
Files#walkFileTree(Path,Set,int,FileVisitor)}.
+     * @param linkOptions options to follow links.
+     * @param fileVisitOptions options to configure the traversal
+     * @return Whether the two directories contain the same files without 
considering file contents.
+     * @throws IOException if an I/O error is thrown by a visitor method
+     */
+    public static boolean directoryContentEquals(final Path path1, final Path 
path2, final int maxDepth,
+            LinkOption[] linkOptions, FileVisitOption... fileVisitOptions) 
throws IOException {
+        return new RelativeSortedPaths(path1, path2, maxDepth, linkOptions, 
fileVisitOptions).equals;
+    }
+
+    /**
+     * Compares the file contents of two Paths to determine if they are equal 
or not.
+     * <p>
+     * File content is accessed through {@link 
Files#newInputStream(Path,OpenOption...)}.
+     * </p>
+     *
+     * @param path1 the first stream.
+     * @param path2 the second stream.
+     * @return true if the content of the streams are equal or they both don't 
exist, false otherwise.
+     * @throws NullPointerException if either input is null.
+     * @throws IOException if an I/O error occurs.
+     * @see org.apache.commons.io.FileUtils#contentEquals(java.io.File, 
java.io.File)
+     */
+    public static boolean fileContentEquals(final Path path1, final Path 
path2) throws IOException {
+        return fileContentEquals(path1, path2, EMPTY_LINK_OPTION_ARRAY, 
EMPTY_OPEN_OPTION_ARRAY);
+    }
+
+    /**
+     * Compares the file contents of two Paths to determine if they are equal 
or not.
+     * <p>
+     * File content is accessed through {@link 
Files#newInputStream(Path,OpenOption...)}.
+     * </p>
+     *
+     * @param path1 the first stream.
+     * @param path2 the second stream.
+     * @param linkOptions options specifying how files are followed.
+     * @param openOptions options specifying how files are opened.
+     * @return true if the content of the streams are equal or they both don't 
exist, false otherwise.
+     * @throws NullPointerException if either input is null.
+     * @throws IOException if an I/O error occurs.
+     * @see org.apache.commons.io.FileUtils#contentEquals(java.io.File, 
java.io.File)
+     */
+    public static boolean fileContentEquals(final Path path1, final Path 
path2, final LinkOption[] linkOptions,
+            final OpenOption... openOptions) throws IOException {
+        if (path1 == null && path2 == null) {
+            return true;
+        }
+        if (path1 == null ^ path2 == null) {
+            return false;
+        }
+        final Path nPath1 = path1.normalize();
+        final Path nPath2 = path2.normalize();
+        final boolean path1Exists = Files.exists(nPath1, linkOptions);
+        if (path1Exists != Files.exists(nPath2, linkOptions)) {
+            return false;
+        }
+        if (!path1Exists) {
+            // Two not existing files are equal?
+            // Same as FileUtils
+            return true;
+        }
+        if (Files.isDirectory(nPath1, linkOptions)) {
+            // don't compare directory contents.
+            throw new IOException("Can't compare directories, only files: " + 
nPath1);
+        }
+        if (Files.isDirectory(nPath2, linkOptions)) {
+            // don't compare directory contents.
+            throw new IOException("Can't compare directories, only files: " + 
nPath2);
+        }
+        if (Files.size(nPath1) != Files.size(nPath2)) {
+            // lengths differ, cannot be equal
+            return false;
+        }
+        if (path1.equals(path2)) {
+            // same file
+            return true;
+        }
+        try (final InputStream inputStream1 = Files.newInputStream(nPath1, 
openOptions);
+                final InputStream inputStream2 = Files.newInputStream(nPath2, 
openOptions)) {
+            return IOUtils.contentEquals(inputStream1, inputStream2);
+        }
+    }
+
+    /**
      * Returns whether the given file or directory is empty.
      *
      * @param path the the given file or directory to query.
@@ -274,13 +490,41 @@ public final class PathUtils {
     }
 
     /**
+     * Relativizes all files in the given {@code collection} against a {@code 
parent}.
+     * 
+     * @param collection The collection of paths to relativize.
+     * @param parent relativizes against this parent path.
+     * @param sort Whether to sort the result.
+     * @param comparator How to sort.
+     * @return A collection of relativized paths, optionally sorted.
+     */
+    static List<Path> relativize(Collection<Path> collection, Path parent, 
boolean sort,
+            Comparator<? super Path> comparator) {
+        Stream<Path> stream = collection.stream().map(e -> 
parent.relativize(e));
+        if (sort) {
+            stream = comparator == null ? stream.sorted() : 
stream.sorted(comparator);
+        }
+        return stream.collect(Collectors.toList());
+    }
+
+    /**
+     * Converts an array of {@link FileVisitOption} to a {@link Set}.
+     * 
+     * @param fileVisitOptions input array.
+     * @return a new Set.
+     */
+    static Set<FileVisitOption> toFileVisitOptionSet(final FileVisitOption... 
fileVisitOptions) {
+        return fileVisitOptions == null ? EnumSet.noneOf(FileVisitOption.class)
+                : Arrays.stream(fileVisitOptions).collect(Collectors.toSet());
+    }
+
+    /**
      * Performs {@link Files#walkFileTree(Path,FileVisitor)} and returns the 
given visitor.
      *
      * Note that {@link Files#walkFileTree(Path,FileVisitor)} returns the 
given path.
      *
      * @param visitor See {@link Files#walkFileTree(Path,FileVisitor)}.
      * @param directory See {@link Files#walkFileTree(Path,FileVisitor)}.
-     *
      * @param <T> See {@link Files#walkFileTree(Path,FileVisitor)}.
      * @return the given visitor.
      *
@@ -297,6 +541,26 @@ public final class PathUtils {
      *
      * Note that {@link Files#walkFileTree(Path,FileVisitor)} returns the 
given path.
      *
+     * @param start See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
+     * @param options See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
+     * @param maxDepth See {@link 
Files#walkFileTree(Path,Set,int,FileVisitor)}.
+     * @param visitor See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
+     * @param <T> See {@link Files#walkFileTree(Path,Set,int,FileVisitor)}.
+     * @return the given visitor.
+     *
+     * @throws IOException if an I/O error is thrown by a visitor method
+     */
+    public static <T extends FileVisitor<? super Path>> T visitFileTree(T 
visitor, Path start,
+            Set<FileVisitOption> options, int maxDepth) throws IOException {
+        Files.walkFileTree(start, options, maxDepth, visitor);
+        return visitor;
+    }
+
+    /**
+     * Performs {@link Files#walkFileTree(Path,FileVisitor)} and returns the 
given visitor.
+     *
+     * Note that {@link Files#walkFileTree(Path,FileVisitor)} returns the 
given path.
+     *
      * @param visitor See {@link Files#walkFileTree(Path,FileVisitor)}.
      * @param first See {@link Paths#get(String,String[])}.
      * @param more See {@link Paths#get(String,String[])}.
diff --git 
a/src/test/java/org/apache/commons/io/file/PathUtilsContentEqualsTest.java 
b/src/test/java/org/apache/commons/io/file/PathUtilsContentEqualsTest.java
index 7bcba2e..653a7ce 100644
--- a/src/test/java/org/apache/commons/io/file/PathUtilsContentEqualsTest.java
+++ b/src/test/java/org/apache/commons/io/file/PathUtilsContentEqualsTest.java
@@ -89,4 +89,116 @@ public class PathUtilsContentEqualsTest {
         assertTrue(PathUtils.fileContentEquals(path1, path2));
     }
 
+    @Test
+    public void testDirectoryContentEquals() throws Exception {
+        // Non-existent files
+        final Path path1 = new File(temporaryFolder, getName()).toPath();
+        final Path path2 = new File(temporaryFolder, getName() + "2").toPath();
+        assertTrue(PathUtils.directoryContentEquals(null, null));
+        assertFalse(PathUtils.directoryContentEquals(null, path1));
+        assertFalse(PathUtils.directoryContentEquals(path1, null));
+        // both don't exist
+        assertTrue(PathUtils.directoryContentEquals(path1, path1));
+        assertTrue(PathUtils.directoryContentEquals(path1, path2));
+        assertTrue(PathUtils.directoryContentEquals(path2, path2));
+        assertTrue(PathUtils.directoryContentEquals(path2, path1));
+        // Tree equals true tests
+        {
+            // Trees of files only that contain the same files.
+            final Path dir1 = 
Paths.get("src/test/resources/dir-equals-tests/dir-equals-files-only/directory-files-only1");
+            final Path dir2 = 
Paths.get("src/test/resources/dir-equals-tests/dir-equals-files-only/directory-files-only2");
+            assertTrue(PathUtils.directoryContentEquals(dir1, dir2));
+            assertTrue(PathUtils.directoryContentEquals(dir2, dir2));
+            assertTrue(PathUtils.directoryContentEquals(dir1, dir1));
+            assertTrue(PathUtils.directoryContentEquals(dir2, dir2));
+        }
+        {
+            // Trees of directories containing other directories.
+            final Path dir1 = 
Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/dir1");
+            final Path dir2 = 
Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/dir2");
+            assertTrue(PathUtils.directoryContentEquals(dir1, dir2));
+            assertTrue(PathUtils.directoryContentEquals(dir2, dir2));
+            assertTrue(PathUtils.directoryContentEquals(dir1, dir1));
+            assertTrue(PathUtils.directoryContentEquals(dir2, dir2));
+        }
+        {
+            // Trees of directories containing other directories and files.
+            final Path dir1 = 
Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1");
+            final Path dir2 = 
Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1");
+            assertTrue(PathUtils.directoryContentEquals(dir1, dir2));
+            assertTrue(PathUtils.directoryContentEquals(dir2, dir2));
+            assertTrue(PathUtils.directoryContentEquals(dir1, dir1));
+            assertTrue(PathUtils.directoryContentEquals(dir2, dir2));
+        }
+        // Tree equals false tests
+        {
+            final Path dir1 = 
Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1/directory-files-only1");
+            final Path dir2 = 
Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1/");
+            assertFalse(PathUtils.directoryContentEquals(dir1, dir2));
+            assertFalse(PathUtils.directoryContentEquals(dir2, dir1));
+        }
+        {
+            final Path dir1 = 
Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-and-files");
+            final Path dir2 = 
Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-then-files");
+            assertFalse(PathUtils.directoryContentEquals(dir1, dir2));
+            assertFalse(PathUtils.directoryContentEquals(dir2, dir1));
+        }
+    }
+
+    @Test
+    public void testDirectoryAndFileContentEquals() throws Exception {
+        // Non-existent files
+        final Path path1 = new File(temporaryFolder, getName()).toPath();
+        final Path path2 = new File(temporaryFolder, getName() + "2").toPath();
+        assertTrue(PathUtils.directoryAndFileContentEquals(null, null));
+        assertFalse(PathUtils.directoryAndFileContentEquals(null, path1));
+        assertFalse(PathUtils.directoryAndFileContentEquals(path1, null));
+        // both don't exist
+        assertTrue(PathUtils.directoryAndFileContentEquals(path1, path1));
+        assertTrue(PathUtils.directoryAndFileContentEquals(path1, path2));
+        assertTrue(PathUtils.directoryAndFileContentEquals(path2, path2));
+        assertTrue(PathUtils.directoryAndFileContentEquals(path2, path1));
+        // Tree equals true tests
+        {
+            // Trees of files only that contain the same files.
+            final Path dir1 = 
Paths.get("src/test/resources/dir-equals-tests/dir-equals-files-only/directory-files-only1");
+            final Path dir2 = 
Paths.get("src/test/resources/dir-equals-tests/dir-equals-files-only/directory-files-only2");
+            assertTrue(PathUtils.directoryAndFileContentEquals(dir1, dir2));
+            assertTrue(PathUtils.directoryAndFileContentEquals(dir2, dir2));
+            assertTrue(PathUtils.directoryAndFileContentEquals(dir1, dir1));
+            assertTrue(PathUtils.directoryAndFileContentEquals(dir2, dir2));
+        }
+        {
+            // Trees of directories containing other directories.
+            final Path dir1 = 
Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/dir1");
+            final Path dir2 = 
Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/dir2");
+            assertTrue(PathUtils.directoryAndFileContentEquals(dir1, dir2));
+            assertTrue(PathUtils.directoryAndFileContentEquals(dir2, dir2));
+            assertTrue(PathUtils.directoryAndFileContentEquals(dir1, dir1));
+            assertTrue(PathUtils.directoryAndFileContentEquals(dir2, dir2));
+        }
+        {
+            // Trees of directories containing other directories and files.
+            final Path dir1 = 
Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1");
+            final Path dir2 = 
Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1");
+            assertTrue(PathUtils.directoryAndFileContentEquals(dir1, dir2));
+            assertTrue(PathUtils.directoryAndFileContentEquals(dir2, dir2));
+            assertTrue(PathUtils.directoryAndFileContentEquals(dir1, dir1));
+            assertTrue(PathUtils.directoryAndFileContentEquals(dir2, dir2));
+        }
+        // Tree equals false tests
+        {
+            final Path dir1 = 
Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1/directory-files-only1");
+            final Path dir2 = 
Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1/");
+            assertFalse(PathUtils.directoryAndFileContentEquals(dir1, dir2));
+            assertFalse(PathUtils.directoryAndFileContentEquals(dir2, dir1));
+        }
+        {
+            final Path dir1 = 
Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-and-files");
+            final Path dir2 = 
Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-then-files");
+            assertFalse(PathUtils.directoryAndFileContentEquals(dir1, dir2));
+            assertFalse(PathUtils.directoryAndFileContentEquals(dir2, dir1));
+        }
+    }
+
 }
diff --git 
a/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1/directory-files-only1/file1.txt
 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1/directory-files-only1/file1.txt
new file mode 100644
index 0000000..56a6051
--- /dev/null
+++ 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1/directory-files-only1/file1.txt
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git 
a/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1/directory-files-only1/file2.txt
 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1/directory-files-only1/file2.txt
new file mode 100644
index 0000000..d8263ee
--- /dev/null
+++ 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1/directory-files-only1/file2.txt
@@ -0,0 +1 @@
+2
\ No newline at end of file
diff --git 
a/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1/file1.txt
 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1/file1.txt
new file mode 100644
index 0000000..56a6051
--- /dev/null
+++ 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1/file1.txt
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git 
a/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1/file2.txt
 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1/file2.txt
new file mode 100644
index 0000000..d8263ee
--- /dev/null
+++ 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1/file2.txt
@@ -0,0 +1 @@
+2
\ No newline at end of file
diff --git 
a/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files2/directory-files-only2/file1.txt
 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files2/directory-files-only2/file1.txt
new file mode 100644
index 0000000..56a6051
--- /dev/null
+++ 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files2/directory-files-only2/file1.txt
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git 
a/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files2/directory-files-only2/file2.txt
 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files2/directory-files-only2/file2.txt
new file mode 100644
index 0000000..d8263ee
--- /dev/null
+++ 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files2/directory-files-only2/file2.txt
@@ -0,0 +1 @@
+2
\ No newline at end of file
diff --git 
a/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files2/file1.txt
 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files2/file1.txt
new file mode 100644
index 0000000..56a6051
--- /dev/null
+++ 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files2/file1.txt
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git 
a/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files2/file2.txt
 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files2/file2.txt
new file mode 100644
index 0000000..d8263ee
--- /dev/null
+++ 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files2/file2.txt
@@ -0,0 +1 @@
+2
\ No newline at end of file
diff --git 
a/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/dir1/directory-files-only1/file1.txt
 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/dir1/directory-files-only1/file1.txt
new file mode 100644
index 0000000..56a6051
--- /dev/null
+++ 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/dir1/directory-files-only1/file1.txt
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git 
a/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/dir1/directory-files-only1/file2.txt
 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/dir1/directory-files-only1/file2.txt
new file mode 100644
index 0000000..d8263ee
--- /dev/null
+++ 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/dir1/directory-files-only1/file2.txt
@@ -0,0 +1 @@
+2
\ No newline at end of file
diff --git 
a/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/dir2/directory-files-only1/file1.txt
 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/dir2/directory-files-only1/file1.txt
new file mode 100644
index 0000000..56a6051
--- /dev/null
+++ 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/dir2/directory-files-only1/file1.txt
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git 
a/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/dir2/directory-files-only1/file2.txt
 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/dir2/directory-files-only1/file2.txt
new file mode 100644
index 0000000..d8263ee
--- /dev/null
+++ 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/dir2/directory-files-only1/file2.txt
@@ -0,0 +1 @@
+2
\ No newline at end of file
diff --git 
a/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/directory-files-only1/file1.txt
 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/directory-files-only1/file1.txt
new file mode 100644
index 0000000..56a6051
--- /dev/null
+++ 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/directory-files-only1/file1.txt
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git 
a/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/directory-files-only1/file2.txt
 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/directory-files-only1/file2.txt
new file mode 100644
index 0000000..d8263ee
--- /dev/null
+++ 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/directory-files-only1/file2.txt
@@ -0,0 +1 @@
+2
\ No newline at end of file
diff --git 
a/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/directory-files-only2/file1.txt
 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/directory-files-only2/file1.txt
new file mode 100644
index 0000000..56a6051
--- /dev/null
+++ 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/directory-files-only2/file1.txt
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git 
a/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/directory-files-only2/file2.txt
 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/directory-files-only2/file2.txt
new file mode 100644
index 0000000..d8263ee
--- /dev/null
+++ 
b/src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/directory-files-only2/file2.txt
@@ -0,0 +1 @@
+2
\ No newline at end of file
diff --git 
a/src/test/resources/dir-equals-tests/dir-equals-files-only/directory-files-only1/file1.txt
 
b/src/test/resources/dir-equals-tests/dir-equals-files-only/directory-files-only1/file1.txt
new file mode 100644
index 0000000..56a6051
--- /dev/null
+++ 
b/src/test/resources/dir-equals-tests/dir-equals-files-only/directory-files-only1/file1.txt
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git 
a/src/test/resources/dir-equals-tests/dir-equals-files-only/directory-files-only1/file2.txt
 
b/src/test/resources/dir-equals-tests/dir-equals-files-only/directory-files-only1/file2.txt
new file mode 100644
index 0000000..d8263ee
--- /dev/null
+++ 
b/src/test/resources/dir-equals-tests/dir-equals-files-only/directory-files-only1/file2.txt
@@ -0,0 +1 @@
+2
\ No newline at end of file
diff --git 
a/src/test/resources/dir-equals-tests/dir-equals-files-only/directory-files-only2/file1.txt
 
b/src/test/resources/dir-equals-tests/dir-equals-files-only/directory-files-only2/file1.txt
new file mode 100644
index 0000000..56a6051
--- /dev/null
+++ 
b/src/test/resources/dir-equals-tests/dir-equals-files-only/directory-files-only2/file1.txt
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git 
a/src/test/resources/dir-equals-tests/dir-equals-files-only/directory-files-only2/file2.txt
 
b/src/test/resources/dir-equals-tests/dir-equals-files-only/directory-files-only2/file2.txt
new file mode 100644
index 0000000..d8263ee
--- /dev/null
+++ 
b/src/test/resources/dir-equals-tests/dir-equals-files-only/directory-files-only2/file2.txt
@@ -0,0 +1 @@
+2
\ No newline at end of file

Reply via email to