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}