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