001package Torello.Java; 002 003import static Torello.Java.C.*; 004 005import java.util.*; 006import java.io.*; 007import java.nio.file.*; 008 009/** 010 * Operating-System independent utilities for moving, copying and deleting files, or an entire 011 * tree of files - using the <CODE>FileNode</CODE> class. 012 * 013 * <EMBED CLASS='external-html' DATA-FILE-ID=FILE_TRANSFER> 014 */ 015@Torello.JavaDoc.StaticFunctional 016public class FileTransfer 017{ 018 private FileTransfer() { } 019 020 /** 021 * Copies the contents of one directory to another. Avoids copying files that do not pass 022 * the filter test. This method will only copy files and will avoid copying any sub-directories 023 * to the target directory. 024 * 025 * <DIV CLASS="EXAMPLE">{@code 026 * // This loads all available files in the 'javadoc/' directory-tree for package Torello.HTML 027 * FileNode fn = FileNode 028 * .createRoot("javadoc/Torello/HTML/") 029 * .loadTree(); 030 * 031 * // This will copy all '.html' files in directory 'javadoc/Torello/HTML/' to a temp directory. 032 * // Log information (copy messages) will be printed to standard-out. If the programmer is on a 033 * // UNIX system, they will be colorized using UNIX terminal color-codes. 034 * // NOTE: The directory 'temp/' must have already been created. 035 * FileTransfer.copy(fn, f -> f.name.endsWith(".html"), "temp/", System.out); 036 * }</DIV> 037 * 038 * @param directory This must be a "directory" not a "file" instance of {@code public class 039 * FileNode} or an exception shall be thrown. If this class is a directory, then every file that 040 * is currently in this directory shall be copied to the {@code 'targetDirectory.'} This 041 * parameter may not be null. 042 * 043 * @param filter This parameter may be null, but if it is not, each file will be tested by this 044 * java-lambda for identifying whether or not it meets the "accept" or "reject" interface before 045 * copying is performed. 046 * 047 * <BR /><BR /><B><SPAN STYLE="color: red;">FILTER PREDICATE BEHAVIOR:</SPAN></B> This filter 048 * {@code Predicate<FileNode>} will return {@code TRUE} to indicate that the file in question 049 * should be copied, and {@code FALSE} if no copy is needed. 050 * 051 * @param targetDirectory Where the files shall be copied. This must be a valid directory as 052 * well, or else a {@code FileNotFoundException} shall throw. This method <B>will not</B> 053 * create this directory if it does not exist. See method {@code java.io.File.mkdirs()} for 054 * information on how to create files on the file-system. 055 * 056 * <BR /><BR /><B>NOTE:</B> It is expected that the last character in this {@code String} 057 * contain a directory-separator character (on UNIX, this is the forward-slash ({@code '/'}) and 058 * in MS-DOS, Windows this is the back-slash ({@code '\'}). If this character is not present, if 059 * this {@code String} does not 'end-with' a {@code java.io.File.separator} then one will be 060 * appended to the end of this {@code String}. 061 * 062 * @param a This parameter may be null, but if it is not, then debugging / logging / 063 * informational messages will be sent to this output. 064 * 065 * <EMBED CLASS='external-html' DATA-FILE-ID=APPENDABLE> 066 * 067 * @return The number of files that were copied. 068 * 069 * @throws DirExpectedException If you pass a "file" instance of {@code class FileNode} to 070 * parameter {@code 'directory.'} 071 * 072 * @throws WritableDirectoryException If the target-directory is not available to Java for 073 * copying. 074 * 075 * @throws java.nio.file.NoSuchFileException This will be thrown if the logic which checks to 076 * ensure that the source and target directories are not identical is unable to identify the 077 * <I><B>real path name</B></I> of either the source or target directory. One such possible 078 * situation where this would happen would be if the user applied the UNIX <B>tilda 079 * ({@code '~'})</B> in either the {@code source} or {@code target} directory-name. 080 * 081 * @throws java.nio.file.InvalidPathException This will be thrown if {@code class 082 * java.nio.file.Paths} is unable to instantiate a {@code java.nio.file.Path} for either the 083 * source-directory (parameter {@code directory}), or the {@code targetDirectory}. 084 * 085 * @throws SameSourceAndTargetException This will be thrown if the source and target 086 * directories are found to point to identical locations on the file-system. 087 * 088 * @throws IOException For any IO filesystem errors. 089 * 090 * @see #copyRecursive(FileNode, FileNodeFilter, FileNodeFilter, String, Appendable) 091 * @see FileNode#getDirContentsFiles(RTC) 092 * @see DirExpectedException#check(FileNode) 093 * @see WritableDirectoryException#check(String) 094 * @see SameSourceAndTargetException#check(FileNode, String) 095 */ 096 public static int copy 097 (FileNode directory, FileNodeFilter filter, String targetDirectory, Appendable a) 098 throws IOException, SameSourceAndTargetException, InvalidPathException, NoSuchFileException 099 { 100 // The purpose of "INTERNAL" here is that we only need to check the input ONCE. 101 102 DirExpectedException.check(directory); 103 WritableDirectoryException.check(targetDirectory); 104 SameSourceAndTargetException.check(directory, targetDirectory); 105 106 if (! targetDirectory.endsWith(File.separator)) 107 targetDirectory = targetDirectory + File.separator; 108 109 return copyINTERNAL(directory, filter, targetDirectory, a); 110 } 111 112 //Internal Method. Skips Retesting Input Validity, during recursion. 113 private static int copyINTERNAL 114 (FileNode directory, FileNodeFilter filter, String targetDirectory, Appendable a) 115 throws IOException 116 { 117 int count = 0; 118 Iterator<FileNode> iter = directory.getDirContentsFiles(RTC.ITERATOR()); 119 120 while (iter.hasNext()) 121 { 122 FileNode fn = iter.next(); 123 124 if ((filter != null) && (! filter.test(fn))) continue; 125 126 copyFileINTERNAL(fn.getFullPathName(), targetDirectory + fn.name); 127 128 if (a != null) a.append( 129 BCYAN + "COPY: " + RESET + fn.getFullPathName() + BCYAN + "\tTO:\t" + 130 RESET + targetDirectory + fn.name + '\n' 131 ); 132 133 count++; 134 } 135 136 return count; 137 } 138 139 /** 140 * Copies an entire directory tree to a target-directory. Note, this class will use the 141 * {@code java.io.File.mkdirs()} method to create any sub-directories that exists in the 142 * Source-Directory Tree, but not in the Target-Directory Tree. 143 * 144 * @param directory This is the source or "root node" of the directory-tree that needs to be 145 * (recursively) copied to the {@code 'targetDirectory'}. This {@code FileNode} must be a 146 * directory, or else an {@code DirExpectedException} will be thrown. This parameter may not 147 * be null. 148 * 149 * @param fileFilter If the programmer using this method would like to maintain some control 150 * in deciding which files are copied - copying some, but not others - to the destination / 151 * target-directory, the provide a Java {@code Predicate} which makes decisions on which files 152 * to copy to the target-directory, and which files to leave out of the copy process. 153 * 154 * <BR /><BR /><B><SPAN STYLE="color: red;">FILTER PREDICATE BEHAVIOR:</SPAN></B> This filter 155 * {@code Predicate<FileNode>} will return {@code TRUE} to indicate that the file in question 156 * should be copied, and {@code FALSE} if no copy is needed. 157 * 158 * <BR /><BR /><B>NOTE:</B> This parameter may be null, and if it is it shall just be ignored. 159 * This is the default case, and <I>all files found in each-and-every-level of the source 160 * directory tree will be copied</I> to the target-directory. 161 * 162 * @param dirFilter If the programmer using this method would like to skip <I>entire branches 163 * of the source directory tree</I>, then implement a java predicate that identifies which 164 * sub-directories (read: 'branches') of the source directory-tree should be skipped. This 165 * parameter (a {@code java.util.function.Predicate<FileNode>}) shall receive file-system 166 * directories (not files!) as input to its {@code public boolean test()} method, and if this 167 * method returns {@code FALSE}, the branch that was passed to the {@code Predicate} shall be 168 * skipped entirely by this copy-routine. 169 * 170 * <BR /><BR /><B><SPAN STYLE="color: red;">FILTER PREDICATE BEHAVIOR:</SPAN></B> This filter 171 * {@code Predicate<FileNode>} will return {@code TRUE} to indicate that the sub-directory in 172 * question should be <I>traversed <B>and</B> copied,</I> and {@code FALSE} in order to 173 * <I><B>skip</B> the provided sub-directory entirely</I> from the copy process. 174 * 175 * <BR /><BR /><B>NOTE:</B> If this parameter is null, it shall just be ignored. This is the 176 * default setting, and when this parameter is null, all branches (all sub-directories) of the 177 * {@code FileNode} parameter {@code 'fn'} shall be copied to {@code 'targetDirectory'.} 178 * 179 * <BR /><BR /><B><SPAN STYLE="color: red;">SUBTLE NOTE:</B></SPAN> The following two examples 180 * will hopefully clarify an issue about filters. The parameter {@code 'fileFilter'} is, in 181 * most cases, more useable than the {@code 'dirFilter'} parameter. The {@code 'dirFilter'} 182 * will force the code <B>skip</B> an entire branch of the directory tree without traversing 183 * it. See this example below where the programmer is attempting to copy all files/classes in 184 * the {@code javadoc/Torello/HTML/NodeSearch/} directory that have the word 'TagNode' 185 * 186 * <DIV CLASS="EXAMPLE-SCROLL">{@code 187 * // These lines will load the "javadoc" documentation files for this jar into a FileNode Tree. 188 * // These are mostly the '.html' files for the code-documentation you are reading right now. 189 * FileNode fn = FileNode 190 * .createRoot("javadoc/") 191 * .loadTree(); 192 * 193 * // THIS WILL FAIL to copy the "TagNode" files in the "NodeSearch" directory to 'temp/' 194 * FileTransfer.copyRecursive( 195 * fn, 196 * f -> f.toString().contains("TagNode"), // Expecting any file having "TagNode" in it's name 197 * f -> f.toString().contains("NodeSearch"), // Supposedly expecting directories having "NodeSearch" in their name 198 * "temp/", System.out 199 * ); 200 * // FAILS TO COPY ANY FILES: The directory predicate BLOCKED the first sub-directory from loading! 201 * 202 * // THIS WILL SUCCEED 203 * FileTransfer.copyRecursive( 204 * fn, 205 * f -> StrCmpr.containsAND(f.toString(), "TagNode", "NodeSearch"), 206 * null, 207 * "temp/", System.out 208 * ); 209 * // This SUCCEEDS since the f.toString() method will return the FULL PATH NAME of any file. 210 * // File's having "TagNode" or "NodeSearch" in their file name, or sub-directory path will result 211 * // in the file predicate returning TRUE. 16 Files here will be copied to 'temp/' 212 * // NOTE: The files are javadoc/Torello/HTML/NodeSearch/TagNodeFind.html, and others... 213 * }</DIV> 214 * 215 * @param targetDirectory Where the files shall be copied. This must be a valid directory as 216 * well, or else a {@code FileNotFoundException} shall throw. This method <B>will not</B> 217 * create this directory if it does not exist. See method {@code java.io.File.mkdirs()} for 218 * information on how to create files on the file-system. 219 * 220 * <BR /><BR /><B>NOTE:</B> It is expected that the last character in this {@code String} 221 * contain a directory-separator character (on UNIX, this is the forward-slash ({@code '/'}) 222 * and in MS-DOS, Windows this is the back-slash ({@code '\'}). If this character is not 223 * present, if this {@code String} does not 'end-with' a {@code java.io.File.separator} then 224 * one will be appended to the end of this {@code String}. 225 * 226 * @param a This parameter may be null, but if it is not, then debugging / logging / 227 * informational messages will be sent to this output. 228 * 229 * @return This method makes calls to the single-level, single-directory-version of the 230 * {@code copy(...)} method in this class for each directory found in the tree. This method 231 * shall sum-up all and count all the files as they are copied. The value returned by this 232 * method is an integer specified how many files were copied in the process. 233 * 234 * @throws DirExpectedException If you pass a "file" instance of {@code class FileNode} to 235 * parameter {@code 'directory.'} 236 * 237 * @throws WritableDirectoryException If the initial target-directory, itself, is not available 238 * to Java for copying, then this exception shall throw. In actuality, all sub-directories 239 * that need to be created will be created by this recursive-copy operation - except for the 240 * highest-level "top directory" (the one indicated by the parameter {@code 'targetDirectory'} 241 * - because if that doesn't exist, then this {@code 'WritableDirectoryException'} will throw). 242 * 243 * @throws java.nio.file.NoSuchFileException This will be thrown if the logic which checks to 244 * ensure that the source and target directories are not identical is unable to identify the 245 * <I><B>real path name</B></I> of either the source or target directory. One such possible 246 * situation where this would happen would be if the user applied the UNIX <B>tilda 247 * ({@code '~'})</B> in either the {@code source} or {@code target} directory-name. 248 * 249 * @throws java.nio.file.InvalidPathException This will be thrown if {@code class 250 * java.nio.file.Paths} is unable to instantiate a {@code java.nio.file.Path} for either the 251 * source-directory (parameter {@code directory}), or the {@code targetDirectory}. 252 * 253 * @throws SameSourceAndTargetException This will be thrown if the source and target 254 * directories are found to point to identical locations on the file-system. 255 * 256 * @throws IOException For any IO filesystem errors. 257 * 258 * @see #copy(FileNode, FileNodeFilter, String, Appendable) 259 * @see FileNode#getDirContentsDirs(RTC) 260 * @see DirExpectedException#check(FileNode) 261 * @see WritableDirectoryException#check(String) 262 * @see SameSourceAndTargetException#check(FileNode, String) 263 */ 264 public static int copyRecursive( 265 FileNode directory, FileNodeFilter fileFilter, FileNodeFilter dirFilter, 266 String targetDirectory, Appendable a 267 ) 268 throws IOException, SameSourceAndTargetException, InvalidPathException, NoSuchFileException 269 { 270 // The purpose of "INTERNAL" here is that we only need to check the input ONCE. 271 272 DirExpectedException.check(directory); 273 WritableDirectoryException.check(targetDirectory); 274 SameSourceAndTargetException.check(directory, targetDirectory); 275 276 if (! targetDirectory.endsWith(File.separator)) 277 targetDirectory = targetDirectory + File.separator; 278 279 return copyRecursiveINTERNAL(directory, fileFilter, dirFilter, targetDirectory, a); 280 } 281 282 // Internal Method. Skips Retesting Input Validity, during recursion. 283 private static int copyRecursiveINTERNAL( 284 FileNode directory, FileNodeFilter fileFilter, FileNodeFilter dirFilter, 285 String targetDirectory, Appendable a 286 ) 287 throws IOException 288 { 289 int numCopied = copyINTERNAL(directory, fileFilter, targetDirectory, a); 290 291 if (a != null) a.append( 292 "Copied " + StringParse.zeroPad(numCopied) + " files from:" + 293 BRED + " [" + directory.getFullPathName() + "]" + RESET + 294 " to: " + BRED + "[" + targetDirectory + "]" + RESET + '\n' 295 ); 296 297 Iterator<FileNode> dirs = directory.getDirContentsDirs(RTC.ITERATOR()); 298 while (dirs.hasNext()) 299 { 300 FileNode dir = dirs.next(); 301 if ((dirFilter != null) && (! dirFilter.test(dir))) continue; 302 303 String newTargetDirectory = targetDirectory + dir.name + File.separator; 304 new File(newTargetDirectory).mkdirs(); 305 numCopied += copyRecursiveINTERNAL(dir, fileFilter, dirFilter, newTargetDirectory, a); 306 } 307 308 return numCopied; 309 } 310 311 /** 312 * This method will delete files (not sub-directories) from a source directory. This method is 313 * not recursive, and will not delete files from sub-directories. Files in all other 314 * directories of the directory tree that begins with parameter {@code FileNode 'directory'} 315 * shall all be left-alone, except the files inside the contents of parameter 316 * {@code FileNode 'directory'} itself. 317 * 318 * @param directory This is the source or "root node" of the directory-tree that needs to be 319 * (recursively) copied to the {@code targetDirectory.} This {@code FileNode} must be a 320 * directory, or else a {@code DirExpectedException} will be thrown. This parameter may not be 321 * null. 322 * 323 * @param filter If the programmer using this method would like to maintain some control in 324 * deciding which files are deleted - deleting some, but not others - from {@code 'directory'}, 325 * then he / she should provide a {@code java.util.function.Predicate<FileNode>} here using the 326 * {@code 'filter'} parameter which makes decisions on which files to delete, and which to 327 * leave alone. 328 * 329 * <BR /><BR /><B><SPAN STYLE="color: red;">FILTER PREDICATE BEHAVIOR:</SPAN></B> This filter 330 * {@code Predicate<FileNode>} will return {@code TRUE} to indicate that the file in question 331 * should be deleted, and {@code FALSE} if no delete is needed. 332 * 333 * <BR /><BR /><B>NOTE:</B> This parameter may be null, and if it is it shall just be ignored, 334 * <I>and every file found inside parameter {@code 'directory'} will be removed from the 335 * file-system.</I> 336 * 337 * @param a This parameter may be null, but if it is not, then debugging / logging / 338 * informational messages will be sent to this output. 339 * <EMBED CLASS='external-html' DATA-FILE-ID=APPENDABLE> 340 * 341 * @return An integer that reports how many files were deleted from the file-system. 342 * <BR /><BR /><B><SPAN STYLE="color: red;">NOTE:</B></SPAN> Only files will be deleted by this 343 * method, no directories shall be removed. Also, the only files that are deleted will be the 344 * ones <I>directly found, which are direct-descendants of parameter 345 * {@code FileNode 'directory'}</I> This method is not recursive, and the directory tree will 346 * not be traversed. 347 * 348 * @throws DirExpectedException If you pass a "file" instance of {@code class FileNode} to 349 * parameter {@code 'directory'}. 350 * 351 * @throws IOException For any IO file-system errors. 352 * 353 * @see #deleteFilesRecursive(FileNode, FileNodeFilter, FileNodeFilter, Appendable) 354 * @see FileNode#getDirContentsFiles(RTC) 355 * @see FileNode#getJavaIOFile() 356 * @see DirExpectedException#check(FileNode) 357 */ 358 public static int deleteFiles(FileNode directory, FileNodeFilter filter, Appendable a) 359 throws IOException 360 { 361 // The purpose of "INTERNAL" here is that we only need to check the input ONCE. 362 DirExpectedException.check(directory); 363 return deleteFilesINTERNAL(directory, filter, a); 364 } 365 366 // Internal Method. Skips Retesting Input Validity, during recursion. 367 private static int deleteFilesINTERNAL(FileNode directory, FileNodeFilter filter, Appendable a) 368 throws IOException 369 { 370 int count = 0; 371 Iterator<FileNode> iter = directory.getDirContentsFiles(RTC.ITERATOR()); 372 373 while (iter.hasNext()) 374 { 375 FileNode fn = iter.next(); 376 if ((filter != null) && (! filter.test(fn))) continue; 377 378 File f = fn.getJavaIOFile(); 379 f.delete(); 380 381 if (a != null) a.append("DELETED: " + BCYAN + fn.getFullPathName() + RESET + '\n'); 382 count++; 383 } 384 385 return count; 386 } 387 388 /** 389 * Deletes files sub-directories from a {@code FileNode}. 390 * 391 * <BR /><BR /><B CLASS=JDDescLabel>Deletion Behavior:</B> 392 * 393 * <BR />When, after a delete, a directory or sub-directory is not empty because the 394 * User-Provided Filters requested to skip the deletion of some of the sub-diretories or files, 395 * this method will therefore be unable to remove these sub-directory branches from the 396 * File-System. 397 * 398 * <BR /><BR />This behavior is largely consistent with standard UNIX and MS-DOS commands such 399 * as {@code 'cp', 'mv', 'copy'} etc... Under these Operating-Systems, non-empty directories 400 * cannot be deleted until their contents have been removed completely. 401 * 402 * <BR /><BR /><B CLASS=JDDescLabel>Appendable Log:</B> 403 * 404 * <BR />If a non-null {@code Appendable} (log) is passed to this method, notices are provided 405 * to the user as directories are removed. This may be of use for later reviewing what was 406 * deleted, and what was retained. 407 * 408 * @param directory This is the source or "root node" of the directory-tree that needs to be 409 * (recursively) deleted. This parameter may not be null. This {@code FileNode} must be a 410 * directory, or else a {@code DirExpectedException} will be thrown. 411 * 412 * @param fileFilter If the programmer using this method would like to maintain some control 413 * in deciding which files are deleted, then he/she must provide a Java 414 * {@code java.util.function.Predicate<FileNode>} which makes these decisions regarding which 415 * files to remove, and which files to leave alone. 416 * 417 * <BR /><BR /><B><SPAN STYLE="color: red;">FILTER PREDICATE BEHAVIOR:</SPAN></B> This filter 418 * {@code Predicate<FileNode>} will return {@code TRUE} to indicate that the file in question 419 * should be deleted, and {@code FALSE} if no delete is needed. 420 * 421 * <BR /><BR /><B>NOTE:</B> This parameter may be null, and if it is it shall just be ignored. 422 * This is the default case, and <I>all files found in each-and-every-level of the source 423 * directory tree will be deleted.</I> 424 * 425 * @param dirFilter If the programmer using this method would like to skip <I>entire branches 426 * of the source directory tree</I>, then implement a java predicate that identifies which 427 * sub-directories (read: 'branches') of the source directory-tree should be skipped. This 428 * parameter (a {@code java.util.function.Predicate<FileNode>}) shall receive file-system 429 * directories (not files!) as input to its {@code public boolean test()} method, and if this 430 * method returns {@code FALSE}, the branch that was passed to the {@code Predicate} shall be 431 * skipped entirely by this delete-routine. 432 * 433 * <BR /><BR /><B><SPAN STYLE="color: red;">FILTER PREDICATE BEHAVIOR:</SPAN></B> This filter 434 * {@code Predicate<FileNode>} will return {@code TRUE} to indicate that the sub-directory in 435 * question should be <I>traversed <B>and</B> deleted,</I> and {@code FALSE} in order to 436 * <I><B>skip</B> the provided sub-directory entirely</I> from the delete process. 437 * 438 * <BR /><BR /><B>NOTE:</B> If this parameter is null, it shall just be ignored. This is the 439 * default case, and when this parameter is null, all branches (all sub-directories) of the 440 * {@code FileNode} parameter {@code 'fn'} shall be put through the deletion process. 441 * 442 * @param a This parameter may be null, but if it is not, then debugging / logging / 443 * informational messages will be sent to this output. 444 * 445 * @return This method will return an integer that reports how many files were deleted. It 446 * might be important to note that when sub-directories are deleted, they are only deleted 447 * because they were empty. Any non-empty sub-directory will be left alone, and not removed. 448 * Also, when files are deleted, they add to the "total delete count" which is the output 449 * integer from this method. However, when directories are deleted, their deletion does not 450 * contribute to the output-count. 451 * 452 * @throws DirExpectedException If you pass a "file" instance of {@code class FileNode} to 453 * parameter {@code 'directory'} 454 * 455 * @throws IOException For any IO file-system errors. 456 * 457 * @see #deleteFiles(FileNode, FileNodeFilter, Appendable) 458 * @see FileNode#getDirContentsDirs(RTC) 459 * @see FileNode#getJavaIOFile() 460 * @see DirExpectedException#check(FileNode) 461 */ 462 public static int deleteFilesRecursive( 463 FileNode directory, FileNodeFilter fileFilter, FileNodeFilter dirFilter, 464 Appendable a 465 ) 466 throws IOException 467 { 468 // The purpose of "INTERNAL" here is that we only need to check the input ONCE. 469 DirExpectedException.check(directory); 470 return deleteFilesRecursiveINTERNAL(directory, fileFilter, dirFilter, a); 471 } 472 473 // Internal Method. Skips Retesting Input Validity, during recursion. 474 private static int deleteFilesRecursiveINTERNAL( 475 FileNode directory, FileNodeFilter fileFilter, FileNodeFilter dirFilter, 476 Appendable a 477 ) 478 throws IOException 479 { 480 int numDeleted = deleteFilesINTERNAL(directory, fileFilter, a); 481 482 if (a != null) a.append( 483 "Deleted (" + StringParse.zeroPad(numDeleted) + ") files from:" + 484 BRED + " [" + directory.getFullPathName() + "]" + RESET + '\n' 485 ); 486 487 Iterator<FileNode> dirs = directory.getDirContentsDirs(RTC.ITERATOR()); 488 while (dirs.hasNext()) 489 { 490 FileNode dir = dirs.next(); 491 if ((dirFilter != null) && (! dirFilter.test(dir))) continue; 492 493 numDeleted += deleteFilesRecursiveINTERNAL(dir, fileFilter, dirFilter, a); 494 } 495 496 File d = directory.getJavaIOFile(); 497 int numRemainingFiles = d.list().length; 498 boolean shouldDelete = (dirFilter == null) || dirFilter.test(directory); 499 500 if ((numRemainingFiles == 0) && shouldDelete) 501 { 502 d.delete(); 503 if (a != null) a.append 504 ("Deleted (Empty) Directory: " + directory.getFullPathName() + '\n'); 505 } 506 else if ((a != null) && (numRemainingFiles > 0) && shouldDelete) a.append( 507 "Unable to delete NON-EMPTY Directory: " + directory.getFullPathName() + ", " + 508 numRemainingFiles + " file(s) or dir(s) still remain.\n" 509 ); 510 511 return numDeleted; 512 } 513 514 /** 515 * Moves the contents of one directory to another. Avoids moving files that do not pass the 516 * {@code 'filter'} test. If a log parameter ({@code 'a'}) is provided, textual 517 * status-updates will be printed to that log. 518 * 519 * <BR /><BR /><B CLASS=JDDescLabel>Method Heuristic</B> 520 * 521 * <BR />All this method does is to perform, sequentially: 522 * 523 * <BR /><BR /><OL CLASS=JDOL> 524 * <LI>A copy operation of a file to a new directory.</LI> 525 * <LI>A delete operation on the file from the original location.</LI> 526 * </OL> 527 * 528 * @param directory This must be a "directory" not a "file" instance of 529 * {@code public class FileNode} or an exception shall be thrown. If this class is a 530 * directory, then every file that is currently in this directory shall be copied to the 531 * {@code 'targetDirectory'} - and then deleted from this directory. This parameter may not 532 * be null. 533 * 534 * @param targetDirectory Where the files shall be moved. This must be a valid directory as 535 * well, or else a {@code FileNotFoundException} shall throw. This method <B>will not</B> 536 * create this directory if it does not exist. See method {@code java.io.File.mkdirs()} for 537 * information on how to create files on the file-system. 538 * 539 * <BR /><BR /><B>NOTE:</B> It is expected that the last character in this {@code String} 540 * contain a directory-separator character (on UNIX, this is the forward-slash ({@code '/'}) 541 * and in MS-DOS, Windows this is the back-slash ({@code '\'}). If this character is not 542 * present, if this {@code String} does not 'end-with' a {@code java.io.File.separator} then 543 * one will be appended to the end of this {@code String}. 544 * 545 * @param filter This parameter may be null, but if it is not, each file will be tested by this 546 * java-lambda for identifying whether or not it meets the "accept" or "reject" interface 547 * before moving is performed. 548 * 549 * <BR /><BR /><B><SPAN STYLE="color: red;">FILTER PREDICATE BEHAVIOR:</SPAN></B> This filter 550 * {@code Predicate<FileNode>} will return {@code TRUE} to indicate that the file in question 551 * should be moved, and {@code FALSE} if no move is needed. 552 * 553 * @param a This parameter may be null, but if it is not, then debugging / logging / 554 * informational messages will be sent to this output. 555 * <EMBED CLASS='external-html' DATA-FILE-ID=APPENDABLE> 556 * 557 * @return The number of files that were moved. 558 * 559 * @throws DirExpectedException If you pass a "file" instance of {@code class FileNode} to 560 * parameter {@code 'directory.'} 561 * 562 * @throws WritableDirectoryException If the target-directory is not available to Java for 563 * moving. 564 * 565 * @throws java.nio.file.NoSuchFileException This will be thrown if the logic which checks to 566 * ensure that the source and target directories are not identical is unable to identify the 567 * <I><B>real path name</B></I> of either the source or target directory. One such possible 568 * situation where this would happen would be if the user applied the UNIX <B>tilda 569 * ({@code '~'})</B> in either the {@code source} or {@code target} directory-name. 570 * 571 * <BR /><BR />This check is crucial since when performing a {@code MOVE} operation, the 572 * contents of a directory are <B><I>first copied</I></B>, and <B><I>then deleted</B></I>. If 573 * the source and target directories are identical, then after the initial {@code copy} 574 * operation, the logic would simply {@code delete} the original files. 575 * 576 * @throws java.nio.file.InvalidPathException This will be thrown if {@code class 577 * java.nio.file.Paths} is unable to instantiate a {@code java.nio.file.Path} for either the 578 * source-directory (parameter {@code directory}), or the {@code targetDirectory}. 579 * 580 * @throws SameSourceAndTargetException This will be thrown if the source and target 581 * directories are found to point to identical locations on the file-system. Since this is a 582 * {@code MOVE} operation, when moving files, the logic first copies the files and then deletes 583 * the originals. If the source and target directories are identical, after the initial 584 * {@code COPY} operation completes, the logic would simple erase those originals - <I>which 585 * would destroy both copies!</I> 586 * 587 * @throws IOException For any IO file-system errors. 588 * 589 * @see #moveRecursive(FileNode, FileNodeFilter, FileNodeFilter, String, Appendable) 590 * @see DirExpectedException#check(FileNode) 591 * @see WritableDirectoryException#check(String) 592 * @see SameSourceAndTargetException#check(FileNode, String) 593 */ 594 public static int move 595 (FileNode directory, FileNodeFilter filter, String targetDirectory, Appendable a) 596 throws IOException, SameSourceAndTargetException, InvalidPathException, NoSuchFileException 597 { 598 // The purpose of "INTERNAL" here is that we only need to check the input ONCE. 599 600 DirExpectedException.check(directory); 601 WritableDirectoryException.check(targetDirectory); 602 SameSourceAndTargetException.check(directory, targetDirectory); 603 604 if (! targetDirectory.endsWith(File.separator)) 605 targetDirectory = targetDirectory + File.separator; 606 607 return moveINTERNAL(directory, filter, targetDirectory, a); 608 } 609 610 // Internal Method. Skips Retesting Input Validity, during recursion. 611 private static int moveINTERNAL 612 (FileNode directory, FileNodeFilter filter, String targetDirectory, Appendable a) 613 throws IOException 614 { 615 int count = 0; 616 Iterator<FileNode> iter = directory.getDirContentsFiles(RTC.ITERATOR()); 617 618 while (iter.hasNext()) 619 { 620 FileNode fn = iter.next(); 621 if ((filter != null) && (! filter.test(fn))) continue; 622 623 copyFileINTERNAL(fn.getFullPathName(), targetDirectory + fn.name); 624 625 if (a != null) a.append( 626 BCYAN + "COPY: " + RESET + fn.getFullPathName() + BCYAN + "\tTO:\t" + RESET + 627 targetDirectory + fn.name + '\n' 628 ); 629 630 fn.getJavaIOFile().delete(); 631 632 if (a != null) a.append(BCYAN + "DELETE: " + RESET + fn.getFullPathName() + '\n'); 633 634 count++; 635 } 636 637 return count; 638 } 639 640 /** 641 * Copies an entire directory tree to a target-directory, and the removes the original files 642 * & directories from their original location. Note, this class will use the 643 * {@code java.io.File.mkdirs()'} method to create any sub-directories which are present in the 644 * source-directory-tree but not are not present in the target-directory-tree. 645 * 646 * <BR /><BR /><B CLASS=JDDescLabel>Move Behavior:</B> 647 * 648 * <BR />When, after a move, a sub-directory is not be completely empty because the filters 649 * have elected not to skip moving some of files or sub-directories, then this method be will 650 * be unable to remove the old-copies of those directory-branches of the file-system tree. 651 * 652 * <BR /><BR />This behavior is largely consistent with standard UNIX and MS-DOS commands such 653 * as {@code 'cp', 'mv', 'copy'} etc... Under these Operating-Systems, non-empty directories 654 * cannot be deleted until their contents have been removed completely. 655 * 656 * <BR /><BR /><B CLASS=JDDescLabel>Appendable Log:</B> 657 * 658 * <BR />If a non-null {@code Appendable} (log) is passed to this method, notices are provided 659 * to the user as directories are removed. This may be of use for later reviewing what was 660 * deleted, and what was retained. 661 * 662 * @param directory This is the source or "root node" of the directory-tree that needs to be 663 * (recursively) copied to the {@code 'targetDirectory'}. This {@code FileNode} must be a 664 * directory, or else an {@code DirExpectedException} will be thrown. This parameter may not 665 * be null. 666 * 667 * @param targetDirectory Where the files shall be moved. This must be a valid directory as 668 * well, or else a {@code FileNotFoundException} shall throw. This method <B>will not</B> 669 * create this directory if it does not exist. See method {@code java.io.File.mkdirs()} for 670 * information on how to create files on the file-system. 671 * 672 * <BR /><BR /><B>NOTE:</B> It is expected that the last character in this {@code String} 673 * contain a directory-separator character (on UNIX, this is the forward-slash ({@code '/'}) 674 * and in MS-DOS, Windows this is the back-slash ({@code '\'}). If this character is not 675 * present, if this {@code String} does not 'end-with' a {@code java.io.File.separator} then 676 * one will be appended to the end of this {@code String}. 677 * 678 * @param fileFilter If the programmer using this method would like to maintain some control 679 * in deciding which files are copied - copying some, but not others - to the destination / 680 * target-directory, the provide a Java {@code java.util.function.Predicate<FileNode>} which 681 * makes decisions on which files to copy to {@code 'targetDirectory'}, and which files to 682 * leave out of the copy process. 683 * 684 * <BR /><BR /><B><SPAN STYLE="color: red;">FILTER PREDICATE BEHAVIOR:</SPAN></B> This filter 685 * {@code Predicate<FileNode>} will return {@code TRUE} to indicate that the file in question 686 * should be moved, and {@code FALSE} if no move is needed. 687 * 688 * <BR /><BR /><B>NOTE:</B> This parameter may be null, and if it is it shall just be ignored. 689 * This is the default case, and <I>all files found in each-and-every-level of the source 690 * directory tree will be copied</I> to the {@code targetDirectory}. 691 * 692 * @param dirFilter If the programmer using this method would like to skip <I>entire branches 693 * of the source directory tree</I>, then implement a java predicate that identifies which 694 * sub-directories (read: 'branches') of the source directory-tree should be skipped. 695 * This parameter (a {@code java.util.function.Predicate<FileNode>}) shall receive file-system 696 * directories (not files!) as input to its {@code public boolean test()} method, and if this 697 * method returns {@code FALSE}, the branch that was passed to the {@code Predicate} shall be 698 * skipped entirely by this move-routine. 699 * 700 * <BR /><BR /><B><SPAN STYLE="color: red;">FILTER PREDICATE BEHAVIOR:</SPAN></B> This filter 701 * {@code Predicate<FileNode>} will return {@code TRUE} to indicate that the sub-directory in 702 * question should be <I>traversed <B>and</B> moved</I>, and {@code FALSE} in order to 703 * <I><B>skip</B> the provided sub-directory entirely</I> from the move process. 704 * 705 * <BR /><BR /><B>NOTE:</B> If this parameter is null, it shall just be ignored. This is the 706 * default case, and when this parameter is null, all branches (all sub-directories) of the 707 * {@code FileNode} parameter {@code 'fn'} shall be copied to the target directory. 708 * 709 * @param a This parameter may be null, but if it is not, then debugging / logging / 710 * informational messages will be sent to this output. 711 * 712 * @return This method makes calls to the single-level, single-directory-version of the 713 * {@code 'copy(...)'} method in this class for each directory found in the tree. This method 714 * shall sum-up all and count all the files as they are copied. The value returned by this 715 * method is an integer specified how many files were copied in the process. 716 * 717 * @throws DirExpectedException If you pass a "file" instance of {@code class FileNode} to 718 * parameter {@code 'directory.'} 719 * 720 * @throws WritableDirectoryException If the initial target-directory, itself, is not available 721 * to Java for copying, then this exception shall throw. In actuality, all sub-directories 722 * that need to be created will be created by this recursive-copy operation - except for the 723 * highest-level "top directory" (the one indicated by the parameter {@code 'targetDirectory'} - 724 * because if that doesn't exist, then a {@code 'WritableDirectoryException'} will throw). 725 * 726 * @throws java.nio.file.NoSuchFileException This will be thrown if the logic which checks to 727 * ensure that the source and target directories are not identical is unable to identify the 728 * <I><B>real path name</B></I> of either the source or target directory. One such possible 729 * situation where this would happen would be if the user applied the UNIX <B>tilda 730 * ({@code '~'})</B> in either the {@code source} or {@code target} directory-name. 731 * 732 * <BR /><BR />This check is crucial since when performing a {@code MOVE} operation, the 733 * contents of a directory are <B><I>first copied</I></B>, and <B><I>then deleted</B></I>. If 734 * the source and target directories are identical, then after the initial {@code copy} 735 * operation, the logic would simply {@code delete} the original files. 736 * 737 * @throws java.nio.file.InvalidPathException This will be thrown if {@code class 738 * java.nio.file.Paths} is unable to instantiate a {@code java.nio.file.Path} for either the 739 * source-directory (parameter {@code directory}), or the {@code targetDirectory}. 740 * 741 * @throws SameSourceAndTargetException This will be thrown if the source and target 742 * directories are found to point to identical locations on the file-system. Since this is a 743 * {@code MOVE} operation, when moving files, the logic first copies the files and then deletes 744 * the originals. If the source and target directories are identical, after the initial 745 * {@code COPY} operation completes, the logic would simple erase those originals - <I>which 746 * would destroy both copies!</I> 747 * 748 * @throws IOException For any IO filesystem errors. 749 * 750 * @see #move(FileNode, FileNodeFilter, String, Appendable) 751 * @see FileNode#getDirContentsDirs(RTC) 752 * @see DirExpectedException#check(FileNode) 753 * @see WritableDirectoryException#check(String) 754 * @see SameSourceAndTargetException#check(FileNode, String) 755 */ 756 public static int moveRecursive( 757 FileNode directory, FileNodeFilter fileFilter, FileNodeFilter dirFilter, 758 String targetDirectory, Appendable a 759 ) 760 throws IOException, SameSourceAndTargetException, InvalidPathException, NoSuchFileException 761 { 762 // The purpose of "INTERNAL" here is that we only need to check the input ONCE. 763 764 DirExpectedException.check(directory); 765 WritableDirectoryException.check(targetDirectory); 766 SameSourceAndTargetException.check(directory, targetDirectory); 767 768 if (! targetDirectory.endsWith(File.separator)) 769 targetDirectory = targetDirectory + File.separator; 770 771 return moveRecursiveINTERNAL(directory, fileFilter, dirFilter, targetDirectory, a); 772 } 773 774 // Internal Method. Skips Retesting Input Validity, during recursion. 775 private static int moveRecursiveINTERNAL ( 776 FileNode directory, FileNodeFilter fileFilter, FileNodeFilter dirFilter, 777 String targetDirectory, Appendable a 778 ) 779 throws IOException 780 { 781 int numMoved = moveINTERNAL(directory, fileFilter, targetDirectory, a); 782 783 if (a != null) a.append( 784 "Moved " + StringParse.zeroPad(numMoved) + " files from:" + 785 BRED + " [" + directory.getFullPathName() + "]" + RESET + 786 " to: " + BRED + "[" + targetDirectory + "]" + RESET + '\n' 787 ); 788 789 Iterator<FileNode> dirs = directory.getDirContentsDirs(RTC.ITERATOR()); 790 791 while (dirs.hasNext()) 792 { 793 FileNode dir = dirs.next(); 794 if ((dirFilter != null) && (! dirFilter.test(dir))) continue; 795 796 String newTargetDirectory = targetDirectory + dir.name + File.separator; 797 new File(newTargetDirectory).mkdirs(); 798 799 numMoved += moveRecursiveINTERNAL(dir, fileFilter, dirFilter, newTargetDirectory, a); 800 } 801 802 File d = directory.getJavaIOFile(); 803 int numRemainingFiles = d.list().length; 804 boolean shouldDelete = (dirFilter == null) || dirFilter.test(directory); 805 806 if ((numRemainingFiles == 0) && shouldDelete) 807 { 808 d.delete(); 809 810 if (a != null) 811 a.append("Deleted (Empty) Directory: " + directory.getFullPathName() + '\n'); 812 } 813 814 else if ((a != null) && (numRemainingFiles > 0) && shouldDelete) a.append( 815 "Unable to delete NON-EMPTY Directory: " + directory.getFullPathName() + ", " + 816 numRemainingFiles + " file(s) or dir(s) still remain.\n" 817 ); 818 819 return numMoved; 820 } 821 822 823 private static void copyFileINTERNAL(String inFileName, String outFileOrDirName) 824 throws IOException 825 { 826 try ( 827 FileInputStream fis = new FileInputStream(inFileName); 828 FileOutputStream fos = new FileOutputStream(outFileOrDirName); 829 ) 830 { 831 byte[] b = new byte[5000]; 832 int result = 0; 833 834 while ((result = fis.read(b)) != -1) fos.write(b, 0, result); 835 836 fos.flush(); 837 } 838 } 839}