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     * &amp; 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}