001    package com.softnetConsult.utils.files;
002    
003    import java.io.File;
004    import java.io.FileOutputStream;
005    import java.io.IOException;
006    import java.io.PrintWriter;
007    import java.util.Calendar;
008    import java.util.GregorianCalendar;
009    import java.util.List;
010    
011    
012    /**
013     * This is a collection of static utility methods for dealing with files and parths.
014     * 
015     * <p style="font-size:smaller;">This product includes software developed by the
016     *    <strong>SoftNet-Consult Java Utility Library</strong> project and its contributors.<br />
017     *    (<a href="http://java-tools.sourceforge.net" target="_blank">http://java-tools.sourceforge.net</a>)<br />
018     *    Copyright (c) 2007-2008 SoftNet-Consult.<br />
019     *    Copyright (c) 2007-2008 G. Paperin.<br />
020     *    All rights reserved.
021     * </p>
022     * <p style="font-size:smaller;">File: FileTools.java<br />
023     *    Library API version: {@value com.softnetConsult.utils.APIProperties#apiVersion}<br />
024     *    Java compliance version: {@value com.softnetConsult.utils.APIProperties#javaComplianceVersion}
025     * </p>
026     * <p style="font-size:smaller;">Redistribution and use in source and binary forms, with or
027     *    without modification, are permitted provided that the following terms and conditions are met:
028     * </p>
029     * <p style="font-size:smaller;">1. Redistributions of source code must retain the above
030     *    acknowledgement of the SoftNet-Consult Java Utility Library project, the above copyright
031     *    notice, this list of conditions and the following disclaimer.<br />
032     *    2. Redistributions in binary form must reproduce the above acknowledgement of the
033     *    SoftNet-Consult Java Utility Library project, the above copyright notice, this list of
034     *    conditions and the following disclaimer in the documentation and/or other materials
035     *    provided with the distribution.<br />
036     *    3. All advertising materials mentioning features or use of this software or any derived
037     *    software must display the following acknowledgement:<br />
038     *    <em>This product includes software developed by the SoftNet-Consult Java Utility Library
039     *    project and its contributors.</em>
040     * </p>
041     * <p style="font-size:smaller;">THIS SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY
042     *    OF ANY KIND, EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
043     *    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT SHALL
044     *    THE AUTHORS, CONTRIBUTORS OR COPYRIGHT  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
045     *    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING  FROM, OUT OF OR
046     *    IN CONNECTION WITH THE SOFTWARE OR THE USE OR  OTHER DEALINGS IN THE SOFTWARE.
047     * </p> 
048     * @author Greg Paperin (<a href="http://www.paperin.org" target="_blank">http://www.paperin.org</a>)
049     * @version {@value com.softnetConsult.utils.APIProperties#apiVersion}
050     */
051    public final class FileTools {
052    
053    
054    public static final String FILE_EXTENSION_SEPARATOR = ".";
055    public static final String FILE_NAME_UNIQUE_MAKER_TAG = "%ts";
056    
057    
058    /**
059     * Prevents instances of this class from being created
060     * as this class contains only static utility methods.
061     */
062    private FileTools() {}
063    
064    
065    /**
066     * Gets the extension of the specified file name including the extension
067     * delimeter ({@value #FILE_EXTENSION_SEPARATOR}).
068     * The extension is the part of the file name after the last occurence
069     * of the extension delimeter.
070     * 
071     * @param fileName A file name.
072     * @return The extension of the specified file name including the extension delimeter;
073     * if the file name does not include an extension delimeter, an empty string is returned.
074     */
075    public static String getExtension(String fileName) {
076            return getExtension(fileName, true);
077    }
078    
079    
080    /**
081     * Gets the extension of the specified file name.
082     * The extension is the part of the file name after the last occurence
083     * of the extension delimeter ({@value #FILE_EXTENSION_SEPARATOR}).
084     *  
085     * @param fileName A file name.
086     * @param includeExtSep Whether the extension delimeter
087     * ({@value #FILE_EXTENSION_SEPARATOR}) will be returned as part of the
088     * extension.
089     * @return The extension of the specified file name; if the file name
090     * does not include an extension delimeter, an empty string is returned.
091     */
092    public static String getExtension(String fileName, boolean includeExtSep) {
093            
094            if (null == fileName)
095                    return "";
096            
097            int p = fileName.lastIndexOf(FILE_EXTENSION_SEPARATOR);
098            
099            if (0 > p)
100                    return "";
101            
102            if (includeExtSep)
103                    return fileName.substring(p);
104            return fileName.substring(p + FILE_EXTENSION_SEPARATOR.length());
105    }
106    
107    
108    /**
109     * Gets the extension of the specified file including the extension
110     * delimeter ({@value #FILE_EXTENSION_SEPARATOR}).
111     * The extension is the part of the file name after the last occurence
112     * of the extension delimeter.
113     * 
114     * @param file A file
115     * @return The extension of the specified file including the extension delimeter;
116     * if the file name does not include an extension delimeter, an empty string is returned.
117     */
118    public static String getExtension(File file) {
119            return getExtension(file, true);
120    }
121    
122    
123    /**
124     * Gets the extension of the specified file.
125     * The extension is the part of the file name after the last occurence
126     * of the extension delimeter ({@value #FILE_EXTENSION_SEPARATOR}).
127     * 
128     * @param file A file.
129     * @param includeExtSep Whether the extension delimeter
130     * ({@value #FILE_EXTENSION_SEPARATOR}) will be returned as part of the
131     * extension.
132     * @return The extension of the specified name; if the file name
133     * does not include an extension delimeter, an empty string is returned.
134     */
135    public static String getExtension(File file, boolean includeExtSep) {
136            if (null == file)
137                    return "";
138            return getExtension(file.getName(), includeExtSep);     
139    }
140    
141    
142    /**
143     * Gets the base name of the specified file name.
144     * The base name is the part of the file name before the last occurence
145     * of the extension delimeter ({@value #FILE_EXTENSION_SEPARATOR}).
146     *  
147     * @param fileName A file name.
148     * @return The base name of the specified file name.
149     */
150    public static String getBaseName(String fileName) {
151            int nameIndex = fileName.lastIndexOf(File.separatorChar);
152            String name = fileName.substring(nameIndex + 1);
153            int extIndex = name.lastIndexOf(FILE_EXTENSION_SEPARATOR);
154            if (-1 == extIndex)
155                    return name;
156            return name.substring(0, extIndex);
157    }
158    
159    
160    /**
161     * Gets the base name of the specified name.
162     * The base name is the part of the file name before the last occurence
163     * of the extension delimeter ({@value #FILE_EXTENSION_SEPARATOR}).
164     * 
165     * @param file A file.
166     * @return The base name of the specified file.
167     */
168    public static String getBaseName(File file) {
169            if (null == file)
170                    return "";
171            return getBaseName(file.getName());     
172    }
173    
174    
175    /**
176     * Concatenates a directory name and a file name to build a path by inserting a
177     * file separator ({@code '/'} or {@code '\\'}) between them if necessary.<br />
178     * For example all of the following calls will produce the same result:<br />
179     * - {@code concatDirFile("dir/subdir", "file.name")}<br />
180     * - {@code concatDirFile("dir/subdir/", "file.name")}<br />
181     * - {@code concatDirFile("dir/subdir", "/file.name")}<br />
182     * - {@code concatDirFile("dir/subdir/", "/file.name")}<br />
183     * RESULT: {@code "dir/subdir/file.name"}<br />
184     * Note that {@code '/'} is used in the example, but {@code '\\'} is equally supported.
185     * 
186     * @param dirName A directory name.
187     * @param fileName A file name
188     * @return A file path.
189     */
190    public static final String concatDirFile(String dirName, String fileName) {
191            
192            if (null == dirName)
193                    dirName = "";
194            else
195                    dirName = dirName.trim();
196            
197            if (null == fileName )
198                    fileName = "";
199            else
200                    fileName = fileName.trim();
201            
202            StringBuffer s = new StringBuffer();
203            s.append(dirName);
204            
205            int dirNameLen = s.length();
206            if (0 < dirNameLen) {
207                    
208                    boolean dirEndsWithSeparator = dirName.endsWith(File.separator)
209                                                                            || dirName.endsWith("/")
210                                                                            || dirName.endsWith("\\");
211    
212                    boolean fileStartsWithSeparator = fileName.startsWith(File.separator)
213                                                                               || fileName.startsWith("/")
214                                                                               || fileName.startsWith("\\");
215                    
216                    if (!dirEndsWithSeparator && !fileStartsWithSeparator)
217                            s.append(File.separator);
218                    else if (dirEndsWithSeparator && fileStartsWithSeparator)
219                            s.setLength(dirNameLen - 1);            
220            }
221            
222            s.append(fileName);
223            return s.toString();
224    }
225    
226    
227    /**
228     * Concatenates a file base nameand a file extension to build a full file name
229     * by inserting a file extension separator ({@code '.'}) between them if necessary.<br />
230     * For example all of the following calls will produce the same result:<br />
231     * - {@code concatDirFile("basename", "ext")}<br />
232     * - {@code concatDirFile("basename.", "ext")}<br />
233     * - {@code concatDirFile("basename", ".ext")}<br />
234     * - {@code concatDirFile("basename.", ".ext")}<br />
235     * RESULT: {@code "basename.ext"}<br />
236     * 
237     * @param fileBaseName A file base name.
238     * @param fileExt A file extension.
239     * @return The complete file name.
240     */
241    public static final String concatFileExt(String fileBaseName, String fileExt) {
242            
243            if (null == fileBaseName)
244                    fileBaseName = "";
245            else
246                    fileBaseName = fileBaseName.trim();
247            
248            if (null == fileExt)
249                    fileExt = "";
250            else
251                    fileExt = fileExt.trim();
252            
253            StringBuffer s = new StringBuffer();
254            s.append(fileBaseName);
255            
256            int baseNameLen = s.length();
257            if (0 < baseNameLen) {
258                    
259                    boolean baseEndsWithSeparator = fileBaseName.endsWith(FILE_EXTENSION_SEPARATOR);
260                    boolean extStartsWithSeparator = fileExt.startsWith(FILE_EXTENSION_SEPARATOR);
261                    
262                    if (!baseEndsWithSeparator && !extStartsWithSeparator)
263                            s.append(FILE_EXTENSION_SEPARATOR);
264                    else if (baseEndsWithSeparator && extStartsWithSeparator)
265                            s.setLength(baseNameLen - 1);           
266            }
267            
268            s.append(fileExt);
269            return s.toString();
270    }
271    
272    /**
273     * Creates an absolute file name describing a file with the specified base name and extension
274     * as well as an optional timestamp and an optional counter to make the file
275     * name unique; the program start directory is used for the path.<br />
276     * This methos works generally in the same way as
277     * {@link #findUniqueFileName(String, String, String, boolean)} and is equivalent to
278     * {@code findUniqueFile(fileName, fileExt, alwaysUseTimestamp).getAbsolutePath()}.
279     * 
280     * @param fileName The base name for the unique albolute file name to be created. 
281     * @param fileExt The extension for the unique albolute file name to be created. 
282     * @param alwaysUseTimestamp Whether to use a timestamp if no timestamp position marker
283     * is specified.
284     * @return An absolute file name decribing a path based on the specified parameters
285     * modified as above to make sure that no file with the same name/path already exists. 
286     */
287    public static String findUniqueFileName(String fileName, String fileExt, boolean alwaysUseTimestamp) {
288            return findUniqueFile(fileName, fileExt, alwaysUseTimestamp).getAbsolutePath();
289    }
290    
291    
292    /**
293     * Creates an absolute file name describing a file with the specified base name, extension
294     * and directory as well as an optional timestamp and an optional counter to make the file
295     * name unique.<br />
296     * This methos works generally in the same way as
297     * {@link #findUniqueFileName(String, String, String, boolean)} and is equivalent to
298     * {@code findUniqueFile(fileDir, fileName, fileExt, alwaysUseTimestamp).getAbsolutePath()}.
299     * 
300     * @param fileDir The directory for the unique albolute file name to be created. 
301     * @param fileName The base name for the unique albolute file name to be created. 
302     * @param fileExt The extension for the unique-path {@code File} object to be created. 
303     * @param alwaysUseTimestamp Whether to use a timestamp if no timestamp position marker
304     * is specified.
305     * @return An absolute file name decribing a path based on the specified parameters
306     * modified as above to make sure that no file with the same name/path already exists. 
307     */
308    public static String findUniqueFileName(String fileDir, String fileName, String fileExt,
309                                                                                    boolean alwaysUseTimestamp) {
310            return findUniqueFile(fileDir, fileName, fileExt, alwaysUseTimestamp).getAbsolutePath();
311    }
312    
313    
314    /**
315     * Creates a {@code File} object describing a file with the specified base name and extension
316     * as well as an optional timestamp and an optional counter to make the file
317     * name unique; the program start directory is used for the path.<br />
318     * This methos works generally in the same way as
319     * {@link #findUniqueFileName(String, String, String, boolean)} and is equivalent to
320     * {@code findUniqueFile(System.getProperty("user.dir"), fileName, fileExt, alwaysUseTimestamp)}.
321     * 
322     * @param fileName The base name for the unique-path {@code File} object to be created. 
323     * @param fileExt The extension for the unique-path {@code File} object to be created. 
324     * @param alwaysUseTimestamp Whether to use a timestamp if no timestamp position marker
325     * is specified.
326     * @return A {@code File} object decribing a path based on the specified parameters
327     * modified to make sure that no file with the same name/path already exists. 
328     */
329    public static File findUniqueFile(String fileName, String fileExt, boolean alwaysUseTimestamp) {
330            return findUniqueFile(System.getProperty("user.dir"), fileName, fileExt, alwaysUseTimestamp);
331    }
332    
333    
334    /**
335     * Creates a {@code File} object describing a file with the specified base name, extension
336     * and directory as well as an optional timestamp and an optional counter to make the file
337     * name unique. This method works as follows:<br />
338     * 
339     * 1) A file name is created on the basis of the specified directory, base name and extension.
340     * (e.g. &quot;{@code directory/basename.ext}&quot;).<br />
341     * 2) If the specified file name contains a substring that defines a timestamp marker
342     * (the string &quot;{@value #FILE_NAME_UNIQUE_MAKER_TAG}&quot;), the marker is replaced
343     * by a timestamp based on the current system time. If the specified file name does not
344     * contain a timestamp marker and {@code alwaysUseTimestamp} is {@code true}, a timestamp
345     * is inserted after the filename and before the extension. If the specified file name does not
346     * contain a timestamp marker and {@code alwaysUseTimestamp} is {@code false}, no timespamp is
347     * inserted. Executed on 20 Jan 2008 at 18:30:15 the result may look like this:
348     * &quot;{@code directory/basename.08.01.20-18.30.15.ext}&quot;.<br />
349     * 3) If no physical file with the resulting file name exists already, the corresponding {@code File}
350     * object is returned. If a file with the resulting path exists, a counter is inserted just
351     * before the extension. The result may look like this:
352     * &quot;{@code directory/basename.08.01.20-18.30.15(1).ext}&quot;.<br />
353     * 4) The uniqueness counter is increased (starting with 1) until the resulting file name
354     * does not describe a physical file that already exists.<br />
355     * Note that this method does not attempt to synchronise on the file system, i.e. if this
356     * method is invoked by different processes/threads at the same time the resulting file paths
357     * may or may not be be the same. 
358     *  
359     * @param fileDir The directory for the unique-path {@code File} object to be created. 
360     * @param fileName The base name for the unique-path {@code File} object to be created. 
361     * @param fileExt The extension for the unique-path {@code File} object to be created. 
362     * @param alwaysUseTimestamp Whether to use a timestamp if no timestamp position marker
363     * is specified.
364     * @return A {@code File} object decribing a path based on the specified parameters
365     * modified as above to make sure that no file with the same name/path already exists. 
366     */
367    public static File findUniqueFile(String fileDir, String fileName, String fileExt,
368                                                                      boolean alwaysUseTimestamp) {
369            
370            if (null == fileDir)
371                    fileDir = "";
372            fileDir = fileDir.trim();
373            
374            if (null == fileName)
375                    fileName = "";
376            fileName = fileName.trim();
377            
378            if (null == fileExt)
379                    fileExt = "";
380            fileExt = fileExt.trim();
381            
382            if (!fileDir.endsWith("/") && !fileDir.endsWith("\\"))
383                    fileDir = fileDir + "/";
384            
385            if (!fileExt.startsWith(FILE_EXTENSION_SEPARATOR) && fileExt.length() > 0)
386                    fileExt = FILE_EXTENSION_SEPARATOR + fileExt;
387            
388            if (alwaysUseTimestamp && 0 > fileName.indexOf(FILE_NAME_UNIQUE_MAKER_TAG))
389                    fileName = fileName + ("." + FILE_NAME_UNIQUE_MAKER_TAG);
390            
391            Calendar date = new GregorianCalendar();
392            String uniqueMakerStr = String.format("%1$ty.%1$tm.%1$td-%1$tH.%1$tM.%1$tS", date);
393            
394            StringBuffer uniqueFileName = new StringBuffer();
395            uniqueFileName.append(fileDir);
396            uniqueFileName.append(fileName.replace(FILE_NAME_UNIQUE_MAKER_TAG, uniqueMakerStr));
397            uniqueFileName.append(fileExt);
398            File uniqueFile = new File(uniqueFileName.toString());
399            
400            if (uniqueFile.exists()) {
401                    
402                    int k = 1;
403                    while(uniqueFile.exists()) {
404                            
405                            uniqueFileName.setLength(0);
406                            uniqueFileName.append(fileDir);
407                            uniqueFileName.append(fileName.replace(FILE_NAME_UNIQUE_MAKER_TAG, uniqueMakerStr));
408                            uniqueFileName.append('(');
409                            uniqueFileName.append(k);
410                            uniqueFileName.append(')');
411                            uniqueFileName.append(fileExt);
412                            uniqueFile = new File(uniqueFileName.toString());                       
413                            k++;
414                            if (0 > k)
415                                    throw new RuntimeException("Cannot find a unique file name; last filename tried: "
416                                                                                     + uniqueFile.getAbsolutePath());
417                    }
418            }
419            
420            return uniqueFile;
421    }
422    
423    
424    /**
425     * Writes a specified string to the specified file; the file is always overwritten.
426     * 
427     * @param fileName The name of the file to write to.
428     * @param str The string to write to the file.
429     * @return {@code false} if the specified file name was {@code null} or if an IO exception
430     * occured; {@code true} otherwise.
431     */
432    public static boolean writeToFile(String fileName, String str) {
433            return writeToFile(fileName, str, false);
434    }
435    
436    
437    /**
438     * Writes a specified string to the specified file.
439     * 
440     * @param fileName The name of the file to write to.
441     * @param str The string to write to the file.
442     * @param append If {@code true} than the string will be appended at the end of the file,
443     * otherwise the file will be overwritten. 
444     * @return {@code false} if the specified file name was {@code null} or if an IO exception
445     * occured; {@code true} otherwise.
446     */
447    public static boolean writeToFile(String fileName, String str, boolean append) {
448            
449            if (null == fileName)
450                    return false;
451            
452            try {
453                    PrintWriter out = new PrintWriter(new FileOutputStream(fileName, append));
454                    try             { out.print(str); }
455                    finally { out.close(); }
456            } catch (IOException e) {
457                    return false;
458            }
459            return true;
460    }
461    
462    
463    /**
464     * Deletes the specified file, if the file specifies a directory all contained
465     * files and subdirectories are also deleted recursively. Note that if an error
466     * occurs during the recursive deletion of files contained within a directory,
467     * some of the directory contents and the directory itself may not be deleted.
468     * (This is on contrary to the {@code deleteAllRecursively}-methods in this class
469     * that proceed attempting to delete as many of the contained files and subdirectories
470     * as possible.)
471     * 
472     * @param path The file or directory to delete.
473     * @return If the specified file or directory as well as all contained files and
474     * subdirectories are successfully deleted this method returns {@code true},
475     * otherwise it returns {@code false}.
476     */
477    public static boolean deleteRecursively(String path) {
478            if (null == path)
479                    return false;
480            return deleteRecursively(new File(path));
481    }
482    
483    /**
484     * Deletes the specified file, if the file specifies a directory all contained
485     * files and subdirectories are also deleted recursively. Note that if an error
486     * occurs during the recursive deletion of files contained within a directory,
487     * some of the directory contents and the directory itself may not be deleted.
488     * (This is on contrary to the {@code deleteAllRecursively}-methods in this class
489     * that proceed attempting to delete as many of the contained files and subdirectories
490     * as possible.)
491     * 
492     * @param file The file or directory to delete.
493     * @return If the specified file or directory as well as all contained files and
494     * subdirectories are successfully deleted this method returns {@code true},
495     * otherwise it returns {@code false}.
496     */
497    public static boolean deleteRecursively(File file) {
498            
499            if (null == file)
500                    return false;
501            
502            if (!file.exists())
503                    return false;
504            
505            if (file.isFile())
506                    return file.delete();
507            
508            if (file.isDirectory()) {
509                    
510                    File[] contents = file.listFiles();
511                    if (null == contents)
512                            return false;
513                    
514                    for (File f : contents) {
515                            if (!deleteRecursively(f))
516                                    return false;
517                    }
518                    
519                    return file.delete();
520            }
521            
522            return false;
523    }
524    
525    
526    /**
527     * Deletes the specified file; if the path specifies a directory all contained
528     * files and subdirectories are also deleted recursively. If an error occurs
529     * during the recursive deletion of files this method proceeds attempting to
530     * delete as many of the contained files and subdirectories as possible
531     * (this is on contrary to the {@code deleteRecursively}-methods in this class that
532     * return immediately if an error is encountered).<br />
533     * This method is equivalent to
534     * {@code deleteAllRecursively(new File(path), null, null)}.
535     * 
536     * @param path The file or directory to delete.
537     * @return If the specified file or directory as well as all contained files and
538     * subdirectories are successfully deleted this method returns {@code true},
539     * otherwise it returns {@code false}.
540     */
541    public static boolean deleteAllRecursively(String path) {
542            return deleteAllRecursively(new File(path), null, null);
543    }
544    
545    
546    /**
547     * Deletes the specified file; if the path specifies a directory all contained
548     * files and subdirectories are also deleted recursively. If an error occurs
549     * during the recursive deletion of files this method proceeds attempting to
550     * delete as many of the contained files and subdirectories as possible
551     * (this is on contrary to the {@code deleteRecursively}-methods in this class that
552     * return immediately if an error is encountered).<br />
553     * If the list {@code deleted} is {@code null} it is ignored, otherwise all
554     * files that are successfully deteted will be added to the {@code deleted}-list.
555     * If the list {@code failed} is {@code null} it is ignored, otherwise all files that
556     * could not be successfully deteted will be added to the {@code failed}-list.<br />
557     * This method is equivalent to
558     * {@code deleteAllRecursively(new File(path), deleted, failed)}.
559     * 
560     * @param path The file or directory to delete.
561     * @param deleted A list to which all successfully deleted files are to be added
562     * (may be {@code null} in which case it is ignored).
563     * @param failed A list to which all files that could not be successfully deleted
564     * are to be added (may be {@code null} in which case it is ignored).
565     * @return If the specified file or directory as well as all contained files and
566     * subdirectories are successfully deleted this method returns {@code true},
567     * otherwise it returns {@code false}.
568     */
569    public static boolean deleteAllRecursively(String path, List<File> deleted, List<File> failed) {
570            return deleteAllRecursively(new File(path), deleted, failed);
571    }
572    
573    
574    /**
575     * Deletes the specified file; if the file specifies a directory all contained
576     * files and subdirectories are also deleted recursively. If an error occurs
577     * during the recursive deletion of files this method proceeds attempting to
578     * delete as many of the contained files and subdirectories as possible
579     * (this is on contrary to the {@code deleteRecursively}-methods in this class that
580     * return immediately if an error is encountered).<br />
581     * This method is equivalent to {@code deleteAllRecursively(file, null, null)}.
582     * 
583     * @param file The file or directory to delete.
584     * @return If the specified file or directory as well as all contained files and
585     * subdirectories are successfully deleted this method returns {@code true},
586     * otherwise it returns {@code false}.
587     */
588    public static boolean deleteAllRecursively(File file) {
589            return deleteAllRecursively(file, null, null);
590    }
591    
592    
593    /**
594     * Deletes the specified file; if the file specifies a directory all contained
595     * files and subdirectories are also deleted recursively. If an error occurs
596     * during the recursive deletion of files this method proceeds attempting to
597     * delete as many of the contained files and subdirectories as possible
598     * (this is on contrary to the {@code deleteRecursively}-methods in this class that
599     * return immediately if an error is encountered).<br />
600     * If the list {@code deleted} is {@code null} it is ignored, otherwise all
601     * files that are successfully deteted will be added to the {@code deleted}-list.
602     * If the list {@code failed} is {@code null} it is ignored, otherwise all files
603     * that could not be successfully deteted will be added to the {@code failed}-list.
604     * 
605     * @param file The file or directory to delete.
606     * @param deleted A list to which all successfully deleted files are to be added
607     * (may be {@code null} in which case it is ignored).
608     * @param failed A list to which all files that could not be successfully deleted
609     * are to be added (may be {@code null} in which case it is ignored).
610     * @return If the specified file or directory as well as all contained files and
611     * subdirectories are successfully deleted this method returns {@code true},
612     * otherwise it returns {@code false}.
613     */
614    public static boolean deleteAllRecursively(File file, List<File> deleted, List<File> failed) {
615            
616            if (null == file)
617                    return false;
618            
619            if (!file.exists())
620                    return false;
621            
622            if (file.isFile()) {
623                    if (file.delete()) {
624                            if (null != deleted) deleted.add(file);
625                            return true;
626                    } else {
627                            if (null != failed) failed.add(file);
628                            return false;
629                    }
630            }
631            
632            if (file.isDirectory()) {
633                    
634                    boolean fullSuccess = true;
635                    
636                    File[] contents = file.listFiles();
637                    if (null != contents) {         
638                            for (File f : contents) {
639                                    if (!deleteAllRecursively(f, deleted, failed))
640                                            fullSuccess = false;
641                            }
642                    }
643                    
644                    if (file.delete()) {
645                            if (null != deleted) deleted.add(file);
646                            return fullSuccess;
647                    } else {
648                            if (null != failed) failed.add(file);
649                            return false;
650                    }
651            }
652            
653            return false;
654    }
655    
656    } // public final class FileTools