View Javadoc
1   package org.codehaus.plexus.util;
2   
3   /* ====================================================================
4    * The Apache Software License, Version 1.1
5    *
6    * Copyright (c) 2001 The Apache Software Foundation.  All rights
7    * reserved.
8    *
9    * Redistribution and use in source and binary forms, with or without
10   * modification, are permitted provided that the following conditions
11   * are met:
12   *
13   * 1. Redistributions of source code must retain the above copyright
14   *    notice, this list of conditions and the following disclaimer.
15   *
16   * 2. Redistributions in binary form must reproduce the above copyright
17   *    notice, this list of conditions and the following disclaimer in
18   *    the documentation and/or other materials provided with the
19   *    distribution.
20   *
21   * 3. The end-user documentation included with the redistribution,
22   *    if any, must include the following acknowledgment:
23   *       "This product includes software developed by the
24   *        Apache Software Foundation (http://www.codehaus.org/)."
25   *    Alternately, this acknowledgment may appear in the software itself,
26   *    if and wherever such third-party acknowledgments normally appear.
27   *
28   * 4. The names "Apache" and "Apache Software Foundation" and
29   *    "Apache Turbine" must not be used to endorse or promote products
30   *    derived from this software without prior written permission. For
31   *    written permission, please contact codehaus@codehaus.org.
32   *
33   * 5. Products derived from this software may not be called "Apache",
34   *    "Apache Turbine", nor may "Apache" appear in their name, without
35   *    prior written permission of the Apache Software Foundation.
36   *
37   * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
38   * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
39   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
40   * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
41   * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
42   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
43   * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
44   * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
45   * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
46   * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
47   * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
48   * SUCH DAMAGE.
49   * ====================================================================
50   *
51   * This software consists of voluntary contributions made by many
52   * individuals on behalf of the Apache Software Foundation.  For more
53   * information on the Apache Software Foundation, please see
54   * <http://www.codehaus.org/>.
55   *
56   */
57  
58  import org.codehaus.plexus.util.io.InputStreamFacade;
59  import org.codehaus.plexus.util.io.URLInputStreamFacade;
60  
61  import java.io.BufferedReader;
62  import java.io.File;
63  import java.io.IOException;
64  import java.io.InputStream;
65  import java.io.InputStreamReader;
66  import java.io.OutputStream;
67  import java.io.OutputStreamWriter;
68  import java.io.Reader;
69  import java.io.Writer;
70  import java.net.URL;
71  import java.nio.charset.Charset;
72  import java.nio.file.Files;
73  import java.nio.file.Paths;
74  import java.nio.file.StandardOpenOption;
75  import java.security.SecureRandom;
76  import java.text.DecimalFormat;
77  import java.util.ArrayList;
78  import java.util.Arrays;
79  import java.util.List;
80  import java.util.Random;
81  
82  /**
83   * <p>This class provides basic facilities for manipulating files and file paths.</p>
84   * 
85   * <b>Path-related methods</b>
86   * 
87   * <p>Methods exist to retrieve the components of a typical file path. For example
88   * <code>/www/hosted/mysite/index.html</code>, can be broken into:
89   * <ul>
90   * <li><code>/www/hosted/mysite/</code> -- retrievable through {@link #getPath}</li>
91   * <li><code>index.html</code> -- retrievable through {@link #removePath}</li>
92   * <li><code>/www/hosted/mysite/index</code> -- retrievable through {@link #removeExtension}</li>
93   * <li><code>html</code> -- retrievable through {@link #getExtension}</li>
94   * </ul>
95   * <p>There are also methods to {@link #catPath concatenate two paths}, {@link #resolveFile resolve a path relative to a
96   * File} and {@link #normalize} a path.</p>
97  
98   * <b>File-related methods</b>
99   * 
100  * <p>There are methods to create a {@link #toFile File from a URL}, copy a {@link #copyFileToDirectory File to a
101  * directory}, copy a {@link #copyFile File to another File}, copy a {@link #copyURLToFile URL's contents to a File}, as
102  * well as methods to {@link #deleteDirectory(File) delete} and {@link #cleanDirectory(File) clean} a directory.</p>
103  * 
104  * <p>Common {@link java.io.File} manipulation routines.</p>
105  * 
106  * <p>Taken from the commons-utils repo. Also code from Alexandria's FileUtils. And from Avalon Excalibur's IO. And from
107  * Ant.</p>
108  *
109  * @author <a href="mailto:burton@relativity.yi.org">Kevin A. Burton</A>
110  * @author <a href="mailto:sanders@codehaus.org">Scott Sanders</a>
111  * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
112  * @author <a href="mailto:Christoph.Reck@dlr.de">Christoph.Reck</a>
113  * @author <a href="mailto:peter@codehaus.org">Peter Donald</a>
114  * @author <a href="mailto:jefft@codehaus.org">Jeff Turner</a>
115  *
116  */
117 public class FileUtils
118 {
119     /**
120      * The number of bytes in a kilobyte.
121      */
122     public static final int ONE_KB = 1024;
123 
124     /**
125      * The number of bytes in a megabyte.
126      */
127     public static final int ONE_MB = ONE_KB * ONE_KB;
128 
129     /**
130      * The number of bytes in a gigabyte.
131      */
132     public static final int ONE_GB = ONE_KB * ONE_MB;
133 
134     /**
135      * The vm file separator
136      */
137     public static String FS = File.separator;
138 
139     /**
140      * Non-valid Characters for naming files, folders under Windows: <code>":", "*", "?", "\"", "<", ">", "|"</code>
141      *
142      * @see <a href="http://support.microsoft.com/?scid=kb%3Ben-us%3B177506&x=12&y=13">
143      *      http://support.microsoft.com/?scid=kb%3Ben-us%3B177506&x=12&y=13</a>
144      */
145     private static final String[] INVALID_CHARACTERS_FOR_WINDOWS_FILE_NAME = { ":", "*", "?", "\"", "<", ">", "|" };
146 
147     /**
148      * @return the default excludes pattern
149      * @see DirectoryScanner#DEFAULTEXCLUDES
150      */
151     public static String[] getDefaultExcludes()
152     {
153         return DirectoryScanner.DEFAULTEXCLUDES;
154     }
155 
156     /**
157      * @return the default excludes pattern as list.
158      * @see #getDefaultExcludes()
159      */
160     public static List<String> getDefaultExcludesAsList()
161     {
162         return Arrays.asList( getDefaultExcludes() );
163     }
164 
165     /**
166      * @return the default excludes pattern as comma separated string.
167      * @see DirectoryScanner#DEFAULTEXCLUDES
168      * @see StringUtils#join(Object[], String)
169      */
170     public static String getDefaultExcludesAsString()
171     {
172         return StringUtils.join( DirectoryScanner.DEFAULTEXCLUDES, "," );
173     }
174 
175     /**
176      * Returns a human-readable version of the file size (original is in bytes).
177      *
178      * @param size The number of bytes.
179      * @return A human-readable display value (includes units).
180      */
181     public static String byteCountToDisplaySize( int size )
182     {
183         String displaySize;
184 
185         if ( size / ONE_GB > 0 )
186         {
187             displaySize = String.valueOf( size / ONE_GB ) + " GB";
188         }
189         else if ( size / ONE_MB > 0 )
190         {
191             displaySize = String.valueOf( size / ONE_MB ) + " MB";
192         }
193         else if ( size / ONE_KB > 0 )
194         {
195             displaySize = String.valueOf( size / ONE_KB ) + " KB";
196         }
197         else
198         {
199             displaySize = String.valueOf( size ) + " bytes";
200         }
201 
202         return displaySize;
203     }
204 
205     /**
206      * Returns the directory path portion of a file specification string. Matches the equally named unix command.
207      *
208      * @param filename the file path
209      * @return The directory portion excluding the ending file separator.
210      */
211     public static String dirname( String filename )
212     {
213         int i = filename.lastIndexOf( File.separator );
214         return ( i >= 0 ? filename.substring( 0, i ) : "" );
215     }
216 
217     /**
218      * Returns the filename portion of a file specification string.
219      *
220      * @param filename the file path
221      * @return The filename string with extension.
222      */
223     public static String filename( String filename )
224     {
225         int i = filename.lastIndexOf( File.separator );
226         return ( i >= 0 ? filename.substring( i + 1 ) : filename );
227     }
228 
229     /**
230      * Returns the filename portion of a file specification string. Matches the equally named unix command.
231      *
232      * @param filename the file path
233      * @return The filename string without extension.
234      */
235     public static String basename( String filename )
236     {
237         return basename( filename, extension( filename ) );
238     }
239 
240     /**
241      * Returns the filename portion of a file specification string. Matches the equally named unix command.
242      *
243      * @param filename the file path
244      * @param suffix the file suffix
245      * @return the basename of the file
246      */
247     public static String basename( String filename, String suffix )
248     {
249         int i = filename.lastIndexOf( File.separator ) + 1;
250         int lastDot = ( ( suffix != null ) && ( suffix.length() > 0 ) ) ? filename.lastIndexOf( suffix ) : -1;
251 
252         if ( lastDot >= 0 )
253         {
254             return filename.substring( i, lastDot );
255         }
256         else if ( i > 0 )
257         {
258             return filename.substring( i );
259         }
260         else
261         {
262             return filename; // else returns all (no path and no extension)
263         }
264     }
265 
266     /**
267      * Returns the extension portion of a file specification string. This everything after the last dot '.' in the
268      * filename (NOT including the dot).
269      *
270      * @param filename the file path
271      * @return the extension of the file
272      */
273     public static String extension( String filename )
274     {
275         // Ensure the last dot is after the last file separator
276         int lastSep = filename.lastIndexOf( File.separatorChar );
277         int lastDot;
278         if ( lastSep < 0 )
279         {
280             lastDot = filename.lastIndexOf( '.' );
281         }
282         else
283         {
284             lastDot = filename.substring( lastSep + 1 ).lastIndexOf( '.' );
285             if ( lastDot >= 0 )
286             {
287                 lastDot += lastSep + 1;
288             }
289         }
290 
291         if ( lastDot >= 0 && lastDot > lastSep )
292         {
293             return filename.substring( lastDot + 1 );
294         }
295 
296         return "";
297     }
298 
299     /**
300      * Check if a file exits.
301      *
302      * @param fileName the file path.
303      * @return true if file exists.
304      */
305     public static boolean fileExists( String fileName )
306     {
307         File file = new File( fileName );
308         return file.exists();
309     }
310 
311     /**
312      * Note: the file content is read with platform encoding.
313      *
314      * @param file the file path
315      * @return the file content using the platform encoding.
316      * @throws IOException if any
317      */
318     public static String fileRead( String file )
319         throws IOException
320     {
321         return fileRead( file, null );
322     }
323 
324     /**
325      * @param file the file path
326      * @param encoding the wanted encoding
327      * @return the file content using the specified encoding.
328      * @throws IOException if any
329      */
330     public static String fileRead( String file, String encoding )
331         throws IOException
332     {
333         return fileRead( new File( file ), encoding );
334     }
335 
336     /**
337      * Note: the file content is read with platform encoding
338      *
339      * @param file the file path
340      * @return the file content using the platform encoding.
341      * @throws IOException if any
342      */
343     public static String fileRead( File file )
344         throws IOException
345     {
346         return fileRead( file, null );
347     }
348 
349     /**
350      * @param file the file path
351      * @param encoding the wanted encoding
352      * @return the file content using the specified encoding.
353      * @throws IOException if any
354      */
355     public static String fileRead( File file, String encoding )
356         throws IOException
357     {
358         StringBuilder buf = new StringBuilder();
359 
360         try ( Reader reader = getInputStreamReader( file, encoding ) )
361         {
362             int count;
363             char[] b = new char[512];
364             while ( ( count = reader.read( b ) ) >= 0 ) // blocking read
365             {
366                 buf.append( b, 0, count );
367             }
368         }
369 
370         return buf.toString();
371     }
372 
373     private static InputStreamReader getInputStreamReader( File file, String encoding ) throws IOException
374     {
375         if ( encoding != null )
376         {
377             return new InputStreamReader( Files.newInputStream( file.toPath() ), encoding );
378         }
379         else
380         {
381             return new InputStreamReader( Files.newInputStream( file.toPath() ) );
382         }
383     }
384     
385     /**
386      * Appends data to a file. The file will be created if it does not exist. Note: the data is written with platform
387      * encoding
388      *
389      * @param fileName The path of the file to write.
390      * @param data The content to write to the file.
391      * @throws IOException if any
392      * @deprecated use {@code java.nio.files.Files.write(filename, data.getBytes(encoding),
393      *     StandardOpenOption.APPEND, StandardOpenOption.CREATE)}
394      */
395     public static void fileAppend( String fileName, String data )
396         throws IOException
397     {
398         fileAppend( fileName, null, data );
399     }
400 
401     /**
402      * Appends data to a file. The file will be created if it does not exist.
403      *
404      * @param fileName The path of the file to write.
405      * @param encoding The encoding of the file.
406      * @param data The content to write to the file.
407      * @throws IOException if any
408      * @deprecated use {@code java.nio.files.Files.write(filename, data.getBytes(encoding),
409      *     StandardOpenOption.APPEND, StandardOpenOption.CREATE)}
410      */
411     public static void fileAppend( String fileName, String encoding, String data )
412         throws IOException
413     {
414         try ( OutputStream out = Files.newOutputStream( Paths.get(fileName),
415                 StandardOpenOption.APPEND, StandardOpenOption.CREATE ) )
416         {
417             if ( encoding != null )
418             {
419                 out.write( data.getBytes( encoding ) );
420             }
421             else
422             {
423                 out.write( data.getBytes() );
424             }
425         }
426     }
427 
428     /**
429      * Writes data to a file. The file will be created if it does not exist. Note: the data is written with platform
430      * encoding
431      *
432      * @param fileName The path of the file to write.
433      * @param data The content to write to the file.
434      * @throws IOException if any
435      */
436     public static void fileWrite( String fileName, String data )
437         throws IOException
438     {
439         fileWrite( fileName, null, data );
440     }
441 
442     /**
443      * Writes data to a file. The file will be created if it does not exist.
444      *
445      * @param fileName The path of the file to write.
446      * @param encoding The encoding of the file.
447      * @param data The content to write to the file.
448      * @throws IOException if any
449      */
450     public static void fileWrite( String fileName, String encoding, String data )
451         throws IOException
452     {
453         File file = ( fileName == null ) ? null : new File( fileName );
454         fileWrite( file, encoding, data );
455     }
456 
457     /**
458      * Writes data to a file. The file will be created if it does not exist. Note: the data is written with platform
459      * encoding
460      *
461      * @param file The file to write.
462      * @param data The content to write to the file.
463      * @throws IOException if any
464      * @since 2.0.6
465      */
466     public static void fileWrite( File file, String data )
467         throws IOException
468     {
469         fileWrite( file, null, data );
470     }
471 
472     /**
473      * Writes data to a file. The file will be created if it does not exist.
474      *
475      * @param file The file to write.
476      * @param encoding The encoding of the file.
477      * @param data The content to write to the file.
478      * @throws IOException if any
479      * @since 2.0.6
480      */
481     public static void fileWrite( File file, String encoding, String data )
482         throws IOException
483     {
484         try ( Writer writer = getOutputStreamWriter( file, encoding ) )
485         {
486             writer.write( data );
487         }
488     }
489     
490     private static OutputStreamWriter getOutputStreamWriter( File file, String encoding ) throws IOException
491     {
492         OutputStream out = Files.newOutputStream( file.toPath() );
493         if ( encoding != null )
494         {
495             return new OutputStreamWriter( out, encoding );
496         }
497         else
498         {
499             return new OutputStreamWriter( out );
500         }
501     }
502 
503     /**
504      * Deletes a file.
505      *
506      * @param fileName The path of the file to delete.
507      */
508     public static void fileDelete( String fileName )
509     {
510         File file = new File( fileName );
511         try
512         {
513             NioFiles.deleteIfExists( file );
514         }
515         catch ( IOException e )
516         {
517             throw new RuntimeException( e );
518         }
519     }
520 
521     /**
522      * Waits for NFS to propagate a file creation, imposing a timeout.
523      *
524      * @param fileName The path of the file.
525      * @param seconds The maximum time in seconds to wait.
526      * @return True if file exists.
527      */
528     public static boolean waitFor( String fileName, int seconds )
529     {
530         return waitFor( new File( fileName ), seconds );
531     }
532 
533     /**
534      * Waits for NFS to propagate a file creation, imposing a timeout.
535      *
536      * @param file The file.
537      * @param seconds The maximum time in seconds to wait.
538      * @return True if file exists.
539      */
540     public static boolean waitFor( File file, int seconds )
541     {
542         int timeout = 0;
543         int tick = 0;
544         while ( !file.exists() )
545         {
546             if ( tick++ >= 10 )
547             {
548                 tick = 0;
549                 if ( timeout++ > seconds )
550                 {
551                     return false;
552                 }
553             }
554             try
555             {
556                 Thread.sleep( 100 );
557             }
558             catch ( InterruptedException ignore )
559             {
560                 // nop
561             }
562         }
563         return true;
564     }
565 
566     /**
567      * Creates a file handle.
568      *
569      * @param fileName The path of the file.
570      * @return A <code>File</code> manager.
571      */
572     public static File getFile( String fileName )
573     {
574         return new File( fileName );
575     }
576 
577     /**
578      * <p>Given a directory and an array of extensions return an array of compliant files.</p>
579      * 
580      * <p>TODO Should an ignore list be passed in? TODO Should a recurse flag be passed in?</p>
581      * 
582      * <p>The given extensions should be like "java" and not like ".java"</p>
583      *
584      * @param directory The path of the directory.
585      * @param extensions an array of expected extensions.
586      * @return An array of files for the wanted extensions.
587      */
588     public static String[] getFilesFromExtension( String directory, String[] extensions )
589     {
590         List<String> files = new ArrayList<String>();
591 
592         File currentDir = new File( directory );
593 
594         String[] unknownFiles = currentDir.list();
595 
596         if ( unknownFiles == null )
597         {
598             return new String[0];
599         }
600 
601         for ( String unknownFile : unknownFiles )
602         {
603             String currentFileName = directory + System.getProperty( "file.separator" ) + unknownFile;
604             File currentFile = new File( currentFileName );
605 
606             if ( currentFile.isDirectory() )
607             {
608                 // ignore all CVS directories...
609                 if ( currentFile.getName().equals( "CVS" ) )
610                 {
611                     continue;
612                 }
613 
614                 // ok... transverse into this directory and get all the files... then combine
615                 // them with the current list.
616 
617                 String[] fetchFiles = getFilesFromExtension( currentFileName, extensions );
618                 files = blendFilesToVector( files, fetchFiles );
619             }
620             else
621             {
622                 // ok... add the file
623 
624                 String add = currentFile.getAbsolutePath();
625                 if ( isValidFile( add, extensions ) )
626                 {
627                     files.add( add );
628                 }
629             }
630         }
631 
632         // ok... move the Vector into the files list...
633         return files.toArray( new String[0] );
634     }
635 
636     /**
637      * Private helper method for getFilesFromExtension()
638      */
639     private static List<String> blendFilesToVector( List<String> v, String[] files )
640     {
641         for ( String file : files )
642         {
643             v.add( file );
644         }
645 
646         return v;
647     }
648 
649     /**
650      * Checks to see if a file is of a particular type(s). Note that if the file does not have an extension, an empty
651      * string (&quot;&quot;) is matched for.
652      */
653     private static boolean isValidFile( String file, String[] extensions )
654     {
655         String extension = extension( file );
656         if ( extension == null )
657         {
658             extension = "";
659         }
660 
661         // ok.. now that we have the "extension" go through the current know
662         // excepted extensions and determine if this one is OK.
663 
664         for ( String extension1 : extensions )
665         {
666             if ( extension1.equals( extension ) )
667             {
668                 return true;
669             }
670         }
671 
672         return false;
673 
674     }
675 
676     /**
677      * Simple way to make a directory
678      *
679      * @param dir the directory to create
680      * @throws IllegalArgumentException if the dir contains illegal Windows characters under Windows OS.
681      * @see #INVALID_CHARACTERS_FOR_WINDOWS_FILE_NAME
682      */
683     public static void mkdir( String dir )
684     {
685         File file = new File( dir );
686 
687         if ( Os.isFamily( Os.FAMILY_WINDOWS ) && !isValidWindowsFileName( file ) )
688         {
689             throw new IllegalArgumentException( "The file (" + dir
690                 + ") cannot contain any of the following characters: \n"
691                 + StringUtils.join( INVALID_CHARACTERS_FOR_WINDOWS_FILE_NAME, " " ) );
692         }
693 
694         if ( !file.exists() )
695         {
696             file.mkdirs();
697         }
698     }
699 
700     /**
701      * Compare the contents of two files to determine if they are equal or not.
702      *
703      * @param file1 the first file
704      * @param file2 the second file
705      * @return true if the content of the files are equal or they both don't exist, false otherwise
706      * @throws IOException if any
707      */
708     public static boolean contentEquals( final File file1, final File file2 )
709         throws IOException
710     {
711         final boolean file1Exists = file1.exists();
712         if ( file1Exists != file2.exists() )
713         {
714             return false;
715         }
716 
717         if ( !file1Exists )
718         {
719             // two not existing files are equal
720             return true;
721         }
722 
723         if ( file1.isDirectory() || file2.isDirectory() )
724         {
725             // don't want to compare directory contents
726             return false;
727         }
728         
729         try ( InputStream input1 = Files.newInputStream( file1.toPath() );
730               InputStream input2 = Files.newInputStream( file2.toPath() ) )
731         {
732             return IOUtil.contentEquals( input1, input2 );
733         }
734     }
735 
736     /**
737      * Convert from a <code>URL</code> to a <code>File</code>.
738      *
739      * @param url File URL.
740      * @return The equivalent <code>File</code> object, or <code>null</code> if the URL's protocol is not
741      *         <code>file</code>
742      */
743     public static File toFile( final URL url )
744     {
745         if ( url == null || !url.getProtocol().equalsIgnoreCase( "file" ) )
746         {
747             return null;
748         }
749 
750         String filename = url.getFile().replace( '/', File.separatorChar );
751         int pos = -1;
752         while ( ( pos = filename.indexOf( '%', pos + 1 ) ) >= 0 )
753         {
754             if ( pos + 2 < filename.length() )
755             {
756                 String hexStr = filename.substring( pos + 1, pos + 3 );
757                 char ch = (char) Integer.parseInt( hexStr, 16 );
758                 filename = filename.substring( 0, pos ) + ch + filename.substring( pos + 3 );
759             }
760         }
761         return new File( filename );
762     }
763 
764     /**
765      * Convert the array of Files into a list of URLs.
766      *
767      * @param files the array of files
768      * @return the array of URLs
769      * @throws IOException if an error occurs
770      */
771     public static URL[] toURLs( final File[] files )
772         throws IOException
773     {
774         final URL[] urls = new URL[files.length];
775 
776         for ( int i = 0; i < urls.length; i++ )
777         {
778             urls[i] = files[i].toURI().toURL();
779         }
780 
781         return urls;
782     }
783 
784     /**
785      * Remove extension from filename. ie
786      * 
787      * <pre>
788      * foo.txt    --&gt; foo
789      * a\b\c.jpg  --&gt; a\b\c
790      * a\b\c      --&gt; a\b\c
791      * </pre>
792      *
793      * @param filename the path of the file
794      * @return the filename minus extension
795      */
796     public static String removeExtension( final String filename )
797     {
798         String ext = extension( filename );
799 
800         if ( "".equals( ext ) )
801         {
802             return filename;
803         }
804 
805         final int index = filename.lastIndexOf( ext ) - 1;
806         return filename.substring( 0, index );
807     }
808 
809     /**
810      * Get extension from filename. ie
811      * 
812      * <pre>
813      * foo.txt    --&gt; "txt"
814      * a\b\c.jpg  --&gt; "jpg"
815      * a\b\c      --&gt; ""
816      * </pre>
817      *
818      * @param filename the path of the file
819      * @return the extension of filename or "" if none
820      */
821     public static String getExtension( final String filename )
822     {
823         return extension( filename );
824     }
825 
826     /**
827      * Remove path from filename. Equivalent to the unix command <code>basename</code> ie.
828      * 
829      * <pre>
830      * a/b/c.txt --&gt; c.txt
831      * a.txt     --&gt; a.txt
832      * </pre>
833      *
834      * @param filepath the path of the file
835      * @return the filename minus path
836      */
837     public static String removePath( final String filepath )
838     {
839         return removePath( filepath, File.separatorChar );
840     }
841 
842     /**
843      * Remove path from filename. ie.
844      * 
845      * <pre>
846      * a/b/c.txt --&gt; c.txt
847      * a.txt     --&gt; a.txt
848      * </pre>
849      *
850      * @param filepath the path of the file
851      * @param fileSeparatorChar the file separator character like <b>/</b> on Unix platforms.
852      * @return the filename minus path
853      */
854     public static String removePath( final String filepath, final char fileSeparatorChar )
855     {
856         final int index = filepath.lastIndexOf( fileSeparatorChar );
857 
858         if ( -1 == index )
859         {
860             return filepath;
861         }
862 
863         return filepath.substring( index + 1 );
864     }
865 
866     /**
867      * Get path from filename. Roughly equivalent to the unix command <code>dirname</code>. ie.
868      * 
869      * <pre>
870      * a/b/c.txt --&gt; a/b
871      * a.txt     --&gt; ""
872      * </pre>
873      *
874      * @param filepath the filepath
875      * @return the filename minus path
876      */
877     public static String getPath( final String filepath )
878     {
879         return getPath( filepath, File.separatorChar );
880     }
881 
882     /**
883      * Get path from filename. ie.
884      * 
885      * <pre>
886      * a/b/c.txt --&gt; a/b
887      * a.txt     --&gt; ""
888      * </pre>
889      *
890      * @param filepath the filepath
891      * @param fileSeparatorChar the file separator character like <b>/</b> on Unix platforms.
892      * @return the filename minus path
893      */
894     public static String getPath( final String filepath, final char fileSeparatorChar )
895     {
896         final int index = filepath.lastIndexOf( fileSeparatorChar );
897         if ( -1 == index )
898         {
899             return "";
900         }
901 
902         return filepath.substring( 0, index );
903     }
904 
905     /**
906      * Copy file from source to destination. If <code>destinationDirectory</code> does not exist, it (and any parent
907      * directories) will be created. If a file <code>source</code> in <code>destinationDirectory</code> exists, it will
908      * be overwritten.
909      *
910      * @param source An existing <code>File</code> to copy.
911      * @param destinationDirectory A directory to copy <code>source</code> into.
912      * @throws java.io.FileNotFoundException if <code>source</code> isn't a normal file.
913      * @throws IllegalArgumentException if <code>destinationDirectory</code> isn't a directory.
914      * @throws IOException if <code>source</code> does not exist, the file in <code>destinationDirectory</code> cannot
915      *             be written to, or an IO error occurs during copying.
916      */
917     public static void copyFileToDirectory( final String source, final String destinationDirectory )
918         throws IOException
919     {
920         copyFileToDirectory( new File( source ), new File( destinationDirectory ) );
921     }
922 
923     /**
924      * Copy file from source to destination only if source is newer than the target file. If
925      * <code>destinationDirectory</code> does not exist, it (and any parent directories) will be created. If a file
926      * <code>source</code> in <code>destinationDirectory</code> exists, it will be overwritten.
927      *
928      * @param source An existing <code>File</code> to copy.
929      * @param destinationDirectory A directory to copy <code>source</code> into.
930      * @throws java.io.FileNotFoundException if <code>source</code> isn't a normal file.
931      * @throws IllegalArgumentException if <code>destinationDirectory</code> isn't a directory.
932      * @throws IOException if <code>source</code> does not exist, the file in <code>destinationDirectory</code> cannot
933      *             be written to, or an IO error occurs during copying.
934      */
935     public static void copyFileToDirectoryIfModified( final String source, final String destinationDirectory )
936         throws IOException
937     {
938         copyFileToDirectoryIfModified( new File( source ), new File( destinationDirectory ) );
939     }
940 
941     /**
942      * Copy file from source to destination. If <code>destinationDirectory</code> does not exist, it (and any parent
943      * directories) will be created. If a file <code>source</code> in <code>destinationDirectory</code> exists, it will
944      * be overwritten.
945      *
946      * @param source An existing <code>File</code> to copy.
947      * @param destinationDirectory A directory to copy <code>source</code> into.
948      * @throws java.io.FileNotFoundException if <code>source</code> isn't a normal file.
949      * @throws IllegalArgumentException if <code>destinationDirectory</code> isn't a directory.
950      * @throws IOException if <code>source</code> does not exist, the file in <code>destinationDirectory</code> cannot
951      *             be written to, or an IO error occurs during copying.
952      */
953     public static void copyFileToDirectory( final File source, final File destinationDirectory )
954         throws IOException
955     {
956         if ( destinationDirectory.exists() && !destinationDirectory.isDirectory() )
957         {
958             throw new IllegalArgumentException( "Destination is not a directory" );
959         }
960 
961         copyFile( source, new File( destinationDirectory, source.getName() ) );
962     }
963 
964     /**
965      * Copy file from source to destination only if source is newer than the target file. If
966      * <code>destinationDirectory</code> does not exist, it (and any parent directories) will be created. If a file
967      * <code>source</code> in <code>destinationDirectory</code> exists, it will be overwritten.
968      *
969      * @param source An existing <code>File</code> to copy.
970      * @param destinationDirectory A directory to copy <code>source</code> into.
971      * @throws java.io.FileNotFoundException if <code>source</code> isn't a normal file.
972      * @throws IllegalArgumentException if <code>destinationDirectory</code> isn't a directory.
973      * @throws IOException if <code>source</code> does not exist, the file in <code>destinationDirectory</code> cannot
974      *             be written to, or an IO error occurs during copying.
975      */
976     public static void copyFileToDirectoryIfModified( final File source, final File destinationDirectory )
977         throws IOException
978     {
979         if ( destinationDirectory.exists() && !destinationDirectory.isDirectory() )
980         {
981             throw new IllegalArgumentException( "Destination is not a directory" );
982         }
983 
984         copyFileIfModified( source, new File( destinationDirectory, source.getName() ) );
985     }
986 
987     /**
988      * Creates a number of directories, as delivered from DirectoryScanner
989      * 
990      * @param sourceBase The basedir used for the directory scan
991      * @param dirs The getIncludedDirs from the dirscanner
992      * @param destination The base dir of the output structure
993      * @throws IOException io issue
994      */
995     public static void mkDirs( final File sourceBase, String[] dirs, final File destination )
996         throws IOException
997     {
998         for ( String dir : dirs )
999         {
1000             File src = new File( sourceBase, dir );
1001             File dst = new File( destination, dir );
1002             if ( NioFiles.isSymbolicLink( src ) )
1003             {
1004                 File target = NioFiles.readSymbolicLink( src );
1005                 NioFiles.createSymbolicLink( dst, target );
1006             }
1007             else
1008             {
1009                 dst.mkdirs();
1010             }
1011         }
1012     }
1013 
1014     /**
1015      * Copy file from source to destination. The directories up to <code>destination</code> will be created if they
1016      * don't already exist. <code>destination</code> will be overwritten if it already exists.
1017      *
1018      * @param source An existing non-directory <code>File</code> to copy bytes from.
1019      * @param destination A non-directory <code>File</code> to write bytes to (possibly overwriting).
1020      * @throws IOException if <code>source</code> does not exist, <code>destination</code> cannot be written to, or an
1021      *             IO error occurs during copying.
1022      * @throws java.io.FileNotFoundException if <code>destination</code> is a directory (use
1023      *             {@link #copyFileToDirectory}).
1024      */
1025     public static void copyFile( final File source, final File destination )
1026         throws IOException
1027     {
1028         // check source exists
1029         if ( !source.exists() )
1030         {
1031             final String message = "File " + source + " does not exist";
1032             throw new IOException( message );
1033         }
1034 
1035         // check source != destination, see PLXUTILS-10
1036         if ( source.getCanonicalPath().equals( destination.getCanonicalPath() ) )
1037         {
1038             // if they are equal, we can exit the method without doing any work
1039             return;
1040         }
1041         mkdirsFor( destination );
1042 
1043         doCopyFile( source, destination );
1044 
1045         if ( source.length() != destination.length() )
1046         {
1047             String message = "Failed to copy full contents from " + source + " to " + destination;
1048             throw new IOException( message );
1049         }
1050     }
1051 
1052     private static void doCopyFile( File source, File destination )
1053         throws IOException
1054     {
1055         doCopyFileUsingNewIO( source, destination );
1056     }
1057 
1058     private static void doCopyFileUsingNewIO( File source, File destination )
1059         throws IOException
1060     {
1061         NioFiles.copy( source, destination );
1062     }
1063 
1064     /**
1065      * Link file from destination to source. The directories up to <code>destination</code> will be created if they
1066      * don't already exist. <code>destination</code> will be overwritten if it already exists.
1067      *
1068      * @param source An existing non-directory <code>File</code> to link to.
1069      * @param destination A non-directory <code>File</code> becoming the link (possibly overwriting).
1070      * @throws IOException if <code>source</code> does not exist, <code>destination</code> cannot be created, or an
1071      *             IO error occurs during linking.
1072      * @throws java.io.FileNotFoundException if <code>destination</code> is a directory (use
1073      *             {@link #copyFileToDirectory}).
1074      */
1075     public static void linkFile( final File source, final File destination )
1076         throws IOException
1077     {
1078         // check source exists
1079         if ( !source.exists() )
1080         {
1081             final String message = "File " + source + " does not exist";
1082             throw new IOException( message );
1083         }
1084 
1085         // check source != destination, see PLXUTILS-10
1086         if ( source.getCanonicalPath().equals( destination.getCanonicalPath() ) )
1087         {
1088             // if they are equal, we can exit the method without doing any work
1089             return;
1090         }
1091         mkdirsFor( destination );
1092 
1093         NioFiles.createSymbolicLink( destination, source );
1094     }
1095 
1096     /**
1097      * Copy file from source to destination only if source timestamp is later than the destination timestamp. The
1098      * directories up to <code>destination</code> will be created if they don't already exist. <code>destination</code>
1099      * will be overwritten if it already exists.
1100      *
1101      * @param source An existing non-directory <code>File</code> to copy bytes from.
1102      * @param destination A non-directory <code>File</code> to write bytes to (possibly overwriting).
1103      * @return true if no problem occured
1104      * @throws IOException if <code>source</code> does not exist, <code>destination</code> cannot be written to, or an
1105      *             IO error occurs during copying.
1106      */
1107     public static boolean copyFileIfModified( final File source, final File destination )
1108         throws IOException
1109     {
1110         if ( isSourceNewerThanDestination( source, destination ) )
1111         {
1112             copyFile( source, destination );
1113 
1114             return true;
1115         }
1116 
1117         return false;
1118     }
1119 
1120     /**
1121      * Copies bytes from the URL <code>source</code> to a file <code>destination</code>. The directories up to
1122      * <code>destination</code> will be created if they don't already exist. <code>destination</code> will be
1123      * overwritten if it already exists.
1124      *
1125      * @param source A <code>URL</code> to copy bytes from.
1126      * @param destination A non-directory <code>File</code> to write bytes to (possibly overwriting).
1127      * @throws IOException if
1128      *             <ul>
1129      *             <li><code>source</code> URL cannot be opened</li>
1130      *             <li><code>destination</code> cannot be written to</li>
1131      *             <li>an IO error occurs during copying</li>
1132      *             </ul>
1133      */
1134     public static void copyURLToFile( final URL source, final File destination )
1135         throws IOException
1136     {
1137         copyStreamToFile( new URLInputStreamFacade( source ), destination );
1138     }
1139 
1140     /**
1141      * Copies bytes from the {@link InputStream} <code>source</code> to a file <code>destination</code>. The directories
1142      * up to <code>destination</code> will be created if they don't already exist. <code>destination</code> will be
1143      * overwritten if it already exists.
1144      *
1145      * @param source An {@link InputStream} to copy bytes from. This stream is guaranteed to be closed.
1146      * @param destination A non-directory <code>File</code> to write bytes to (possibly overwriting).
1147      * @throws IOException if
1148      *             <ul>
1149      *             <li><code>source</code> URL cannot be opened</li>
1150      *             <li><code>destination</code> cannot be written to</li>
1151      *             <li>an IO error occurs during copying</li>
1152      *             </ul>
1153      */
1154     public static void copyStreamToFile( final InputStreamFacade source, final File destination )
1155         throws IOException
1156     {
1157         mkdirsFor( destination );
1158         checkCanWrite( destination );
1159 
1160         try (  InputStream input = source.getInputStream();
1161                OutputStream output = Files.newOutputStream( destination.toPath() ) )
1162         {
1163             IOUtil.copy( input, output );
1164         }
1165     }
1166 
1167     private static void checkCanWrite( File destination )
1168         throws IOException
1169     {
1170         // make sure we can write to destination
1171         if ( destination.exists() && !destination.canWrite() )
1172         {
1173             final String message = "Unable to open file " + destination + " for writing.";
1174             throw new IOException( message );
1175         }
1176     }
1177 
1178     private static void mkdirsFor( File destination )
1179     {
1180         // does destination directory exist ?
1181         File parentFile = destination.getParentFile();
1182         if ( parentFile != null && !parentFile.exists() )
1183         {
1184             parentFile.mkdirs();
1185         }
1186     }
1187 
1188     /**
1189      * Normalize a path. Eliminates "/../" and "/./" in a string. Returns <code>null</code> if the ..'s went past the
1190      * root. Eg:
1191      * 
1192      * <pre>
1193      * /foo//               --&gt;     /foo/
1194      * /foo/./              --&gt;     /foo/
1195      * /foo/../bar          --&gt;     /bar
1196      * /foo/../bar/         --&gt;     /bar/
1197      * /foo/../bar/../baz   --&gt;     /baz
1198      * //foo//./bar         --&gt;     /foo/bar
1199      * /../                 --&gt;     null
1200      * </pre>
1201      *
1202      * @param path the path to normalize
1203      * @return the normalized String, or <code>null</code> if too many ..'s.
1204      */
1205     public static String normalize( final String path )
1206     {
1207         String normalized = path;
1208         // Resolve occurrences of "//" in the normalized path
1209         while ( true )
1210         {
1211             int index = normalized.indexOf( "//" );
1212             if ( index < 0 )
1213             {
1214                 break;
1215             }
1216             normalized = normalized.substring( 0, index ) + normalized.substring( index + 1 );
1217         }
1218 
1219         // Resolve occurrences of "/./" in the normalized path
1220         while ( true )
1221         {
1222             int index = normalized.indexOf( "/./" );
1223             if ( index < 0 )
1224             {
1225                 break;
1226             }
1227             normalized = normalized.substring( 0, index ) + normalized.substring( index + 2 );
1228         }
1229 
1230         // Resolve occurrences of "/../" in the normalized path
1231         while ( true )
1232         {
1233             int index = normalized.indexOf( "/../" );
1234             if ( index < 0 )
1235             {
1236                 break;
1237             }
1238             if ( index == 0 )
1239             {
1240                 return null; // Trying to go outside our context
1241             }
1242             int index2 = normalized.lastIndexOf( '/', index - 1 );
1243             normalized = normalized.substring( 0, index2 ) + normalized.substring( index + 3 );
1244         }
1245 
1246         // Return the normalized path that we have completed
1247         return normalized;
1248     }
1249 
1250     /**
1251      * <p>Will concatenate 2 paths. Paths with <code>..</code> will be properly handled.</p>
1252      * 
1253      * Eg.,
1254      * <pre>
1255      * /a/b/c + d = /a/b/d
1256      * /a/b/c + ../d = /a/d
1257      * </pre>
1258 
1259      * <p>Thieved from Tomcat sources...</p>
1260      *
1261      * @param lookupPath a path
1262      * @param path the path to concatenate
1263      * @return The concatenated paths, or null if error occurs
1264      */
1265     public static String catPath( final String lookupPath, final String path )
1266     {
1267         // Cut off the last slash and everything beyond
1268         int index = lookupPath.lastIndexOf( "/" );
1269         String lookup = lookupPath.substring( 0, index );
1270         String pth = path;
1271 
1272         // Deal with .. by chopping dirs off the lookup path
1273         while ( pth.startsWith( "../" ) )
1274         {
1275             if ( lookup.length() > 0 )
1276             {
1277                 index = lookup.lastIndexOf( "/" );
1278                 lookup = lookup.substring( 0, index );
1279             }
1280             else
1281             {
1282                 // More ..'s than dirs, return null
1283                 return null;
1284             }
1285 
1286             index = pth.indexOf( "../" ) + 3;
1287             pth = pth.substring( index );
1288         }
1289 
1290         return new StringBuffer( lookup ).append( "/" ).append( pth ).toString();
1291     }
1292 
1293     /**
1294      * Resolve a file <code>filename</code> to it's canonical form. If <code>filename</code> is relative (doesn't start
1295      * with <code>/</code>), it will be resolved relative to <code>baseFile</code>, otherwise it is treated as a normal
1296      * root-relative path.
1297      *
1298      * @param baseFile Where to resolve <code>filename</code> from, if <code>filename</code> is relative.
1299      * @param filename Absolute or relative file path to resolve.
1300      * @return The canonical <code>File</code> of <code>filename</code>.
1301      */
1302     public static File resolveFile( final File baseFile, String filename )
1303     {
1304         String filenm = filename;
1305         if ( '/' != File.separatorChar )
1306         {
1307             filenm = filename.replace( '/', File.separatorChar );
1308         }
1309 
1310         if ( '\\' != File.separatorChar )
1311         {
1312             filenm = filename.replace( '\\', File.separatorChar );
1313         }
1314 
1315         // deal with absolute files
1316         if ( filenm.startsWith( File.separator ) || ( Os.isFamily( Os.FAMILY_WINDOWS ) && filenm.indexOf( ":" ) > 0 ) )
1317         {
1318             File file = new File( filenm );
1319 
1320             try
1321             {
1322                 file = file.getCanonicalFile();
1323             }
1324             catch ( final IOException ioe )
1325             {
1326                 // nop
1327             }
1328 
1329             return file;
1330         }
1331         // FIXME: I'm almost certain this // removal is unnecessary, as getAbsoluteFile() strips
1332         // them. However, I'm not sure about this UNC stuff. (JT)
1333         final char[] chars = filename.toCharArray();
1334         final StringBuilder sb = new StringBuilder();
1335 
1336         // remove duplicate file separators in succession - except
1337         // on win32 at start of filename as UNC filenames can
1338         // be \\AComputer\AShare\myfile.txt
1339         int start = 0;
1340         if ( '\\' == File.separatorChar )
1341         {
1342             sb.append( filenm.charAt( 0 ) );
1343             start++;
1344         }
1345 
1346         for ( int i = start; i < chars.length; i++ )
1347         {
1348             final boolean doubleSeparator = File.separatorChar == chars[i] && File.separatorChar == chars[i - 1];
1349 
1350             if ( !doubleSeparator )
1351             {
1352                 sb.append( chars[i] );
1353             }
1354         }
1355 
1356         filenm = sb.toString();
1357 
1358         // must be relative
1359         File file = ( new File( baseFile, filenm ) ).getAbsoluteFile();
1360 
1361         try
1362         {
1363             file = file.getCanonicalFile();
1364         }
1365         catch ( final IOException ioe )
1366         {
1367             // nop
1368         }
1369 
1370         return file;
1371     }
1372 
1373     /**
1374      * Delete a file. If file is directory delete it and all sub-directories.
1375      *
1376      * @param file the file path
1377      * @throws IOException if any
1378      */
1379     public static void forceDelete( final String file )
1380         throws IOException
1381     {
1382         forceDelete( new File( file ) );
1383     }
1384 
1385     /**
1386      * Delete a file. If file is directory delete it and all sub-directories.
1387      *
1388      * @param file a file
1389      * @throws IOException if any
1390      */
1391     public static void forceDelete( final File file )
1392         throws IOException
1393     {
1394         if ( file.isDirectory() )
1395         {
1396             deleteDirectory( file );
1397         }
1398         else
1399         {
1400             /*
1401              * NOTE: Always try to delete the file even if it appears to be non-existent. This will ensure that a
1402              * symlink whose target does not exist is deleted, too.
1403              */
1404             boolean filePresent = file.getCanonicalFile().exists();
1405             if ( !deleteFile( file ) && filePresent )
1406             {
1407                 final String message = "File " + file + " unable to be deleted.";
1408                 throw new IOException( message );
1409             }
1410         }
1411     }
1412 
1413     /**
1414      * Accommodate Windows bug encountered in both Sun and IBM JDKs. Others possible. If the delete does not work, call
1415      * System.gc(), wait a little and try again.
1416      *
1417      * @param file a file
1418      * @throws IOException if any
1419      */
1420     private static boolean deleteFile( File file )
1421         throws IOException
1422     {
1423         if ( file.isDirectory() )
1424         {
1425             throw new IOException( "File " + file + " isn't a file." );
1426         }
1427 
1428         if ( !file.delete() )
1429         {
1430             if ( Os.isFamily( Os.FAMILY_WINDOWS ) )
1431             {
1432                 file = file.getCanonicalFile();
1433                 System.gc();
1434             }
1435 
1436             try
1437             {
1438                 Thread.sleep( 10 );
1439                 return file.delete();
1440             }
1441             catch ( InterruptedException ignore )
1442             {
1443                 return file.delete();
1444             }
1445         }
1446 
1447         return true;
1448     }
1449 
1450     /**
1451      * Schedule a file to be deleted when JVM exits. If file is directory delete it and all sub-directories.
1452      *
1453      * @param file a file
1454      * @throws IOException if any
1455      */
1456     public static void forceDeleteOnExit( final File file )
1457         throws IOException
1458     {
1459         if ( !file.exists() )
1460         {
1461             return;
1462         }
1463 
1464         if ( file.isDirectory() )
1465         {
1466             deleteDirectoryOnExit( file );
1467         }
1468         else
1469         {
1470             file.deleteOnExit();
1471         }
1472     }
1473 
1474     /**
1475      * Recursively schedule directory for deletion on JVM exit.
1476      *
1477      * @param directory a directory
1478      * @throws IOException if any
1479      */
1480     private static void deleteDirectoryOnExit( final File directory )
1481         throws IOException
1482     {
1483         if ( !directory.exists() )
1484         {
1485             return;
1486         }
1487         directory.deleteOnExit(); // The hook reverses the list
1488 
1489         cleanDirectoryOnExit( directory );
1490     }
1491 
1492     /**
1493      * Clean a directory without deleting it.
1494      *
1495      * @param directory a directory
1496      * @throws IOException if any
1497      */
1498     private static void cleanDirectoryOnExit( final File directory )
1499         throws IOException
1500     {
1501         if ( !directory.exists() )
1502         {
1503             final String message = directory + " does not exist";
1504             throw new IllegalArgumentException( message );
1505         }
1506 
1507         if ( !directory.isDirectory() )
1508         {
1509             final String message = directory + " is not a directory";
1510             throw new IllegalArgumentException( message );
1511         }
1512 
1513         IOException exception = null;
1514 
1515         final File[] files = directory.listFiles();
1516         for ( final File file : files )
1517         {
1518             try
1519             {
1520                 forceDeleteOnExit( file );
1521             }
1522             catch ( final IOException ioe )
1523             {
1524                 exception = ioe;
1525             }
1526         }
1527 
1528         if ( null != exception )
1529         {
1530             throw exception;
1531         }
1532     }
1533 
1534     /**
1535      * Make a directory.
1536      *
1537      * @param file not null
1538      * @throws IOException If there already exists a file with specified name or the directory is unable to be created
1539      * @throws IllegalArgumentException if the file contains illegal Windows characters under Windows OS.
1540      * @see #INVALID_CHARACTERS_FOR_WINDOWS_FILE_NAME
1541      */
1542     public static void forceMkdir( final File file )
1543         throws IOException
1544     {
1545         if ( Os.isFamily( Os.FAMILY_WINDOWS ) )
1546         {
1547             if ( !isValidWindowsFileName( file ) )
1548             {
1549                 throw new IllegalArgumentException( "The file (" + file.getAbsolutePath()
1550                     + ") cannot contain any of the following characters: \n"
1551                     + StringUtils.join( INVALID_CHARACTERS_FOR_WINDOWS_FILE_NAME, " " ) );
1552             }
1553         }
1554 
1555         if ( file.exists() )
1556         {
1557             if ( file.isFile() )
1558             {
1559                 final String message =
1560                     "File " + file + " exists and is " + "not a directory. Unable to create directory.";
1561                 throw new IOException( message );
1562             }
1563         }
1564         else
1565         {
1566             if ( false == file.mkdirs() )
1567             {
1568                 final String message = "Unable to create directory " + file;
1569                 throw new IOException( message );
1570             }
1571         }
1572     }
1573 
1574     /**
1575      * Recursively delete a directory.
1576      *
1577      * @param directory a directory
1578      * @throws IOException if any
1579      */
1580     public static void deleteDirectory( final String directory )
1581         throws IOException
1582     {
1583         deleteDirectory( new File( directory ) );
1584     }
1585 
1586     /**
1587      * Recursively delete a directory.
1588      *
1589      * @param directory a directory
1590      * @throws IOException if any
1591      */
1592     public static void deleteDirectory( final File directory )
1593         throws IOException
1594     {
1595         if ( !directory.exists() )
1596         {
1597             return;
1598         }
1599 
1600         /*
1601          * try delete the directory before its contents, which will take care of any directories that are really
1602          * symbolic links.
1603          */
1604         if ( directory.delete() )
1605         {
1606             return;
1607         }
1608 
1609         cleanDirectory( directory );
1610         if ( !directory.delete() )
1611         {
1612             final String message = "Directory " + directory + " unable to be deleted.";
1613             throw new IOException( message );
1614         }
1615     }
1616 
1617     /**
1618      * Clean a directory without deleting it.
1619      *
1620      * @param directory a directory
1621      * @throws IOException if any
1622      */
1623     public static void cleanDirectory( final String directory )
1624         throws IOException
1625     {
1626         cleanDirectory( new File( directory ) );
1627     }
1628 
1629     /**
1630      * Clean a directory without deleting it.
1631      *
1632      * @param directory a directory
1633      * @throws IOException if any
1634      */
1635     public static void cleanDirectory( final File directory )
1636         throws IOException
1637     {
1638         if ( !directory.exists() )
1639         {
1640             final String message = directory + " does not exist";
1641             throw new IllegalArgumentException( message );
1642         }
1643 
1644         if ( !directory.isDirectory() )
1645         {
1646             final String message = directory + " is not a directory";
1647             throw new IllegalArgumentException( message );
1648         }
1649 
1650         IOException exception = null;
1651 
1652         final File[] files = directory.listFiles();
1653 
1654         if ( files == null )
1655         {
1656             return;
1657         }
1658 
1659         for ( final File file : files )
1660         {
1661             try
1662             {
1663                 forceDelete( file );
1664             }
1665             catch ( final IOException ioe )
1666             {
1667                 exception = ioe;
1668             }
1669         }
1670 
1671         if ( null != exception )
1672         {
1673             throw exception;
1674         }
1675     }
1676 
1677     /**
1678      * Recursively count size of a directory.
1679      *
1680      * @param directory a directory
1681      * @return size of directory in bytes.
1682      */
1683     public static long sizeOfDirectory( final String directory )
1684     {
1685         return sizeOfDirectory( new File( directory ) );
1686     }
1687 
1688     /**
1689      * Recursively count size of a directory.
1690      *
1691      * @param directory a directory
1692      * @return size of directory in bytes.
1693      */
1694     public static long sizeOfDirectory( final File directory )
1695     {
1696         if ( !directory.exists() )
1697         {
1698             final String message = directory + " does not exist";
1699             throw new IllegalArgumentException( message );
1700         }
1701 
1702         if ( !directory.isDirectory() )
1703         {
1704             final String message = directory + " is not a directory";
1705             throw new IllegalArgumentException( message );
1706         }
1707 
1708         long size = 0;
1709 
1710         final File[] files = directory.listFiles();
1711         for ( final File file : files )
1712         {
1713             if ( file.isDirectory() )
1714             {
1715                 size += sizeOfDirectory( file );
1716             }
1717             else
1718             {
1719                 size += file.length();
1720             }
1721         }
1722 
1723         return size;
1724     }
1725 
1726     /**
1727      * Return the files contained in the directory, using inclusion and exclusion Ant patterns, including the directory
1728      * name in each of the files
1729      *
1730      * @param directory the directory to scan
1731      * @param includes the includes pattern, comma separated
1732      * @param excludes the excludes pattern, comma separated
1733      * @return a list of File objects
1734      * @throws IOException io issue
1735      * @see #getFileNames(File, String, String, boolean)
1736      */
1737     public static List<File> getFiles( File directory, String includes, String excludes )
1738         throws IOException
1739     {
1740         return getFiles( directory, includes, excludes, true );
1741     }
1742 
1743     /**
1744      * Return the files contained in the directory, using inclusion and exclusion Ant patterns
1745      *
1746      * @param directory the directory to scan
1747      * @param includes the includes pattern, comma separated
1748      * @param excludes the excludes pattern, comma separated
1749      * @param includeBasedir true to include the base dir in each file
1750      * @return a list of File objects
1751      * @throws IOException io issue
1752      * @see #getFileNames(File, String, String, boolean)
1753      */
1754     public static List<File> getFiles( File directory, String includes, String excludes, boolean includeBasedir )
1755         throws IOException
1756     {
1757         List<String> fileNames = getFileNames( directory, includes, excludes, includeBasedir );
1758 
1759         List<File> files = new ArrayList<File>();
1760 
1761         for ( String filename : fileNames )
1762         {
1763             files.add( new File( filename ) );
1764         }
1765 
1766         return files;
1767     }
1768 
1769     /**
1770      * Return a list of files as String depending options. This method use case sensitive file name.
1771      *
1772      * @param directory the directory to scan
1773      * @param includes the includes pattern, comma separated
1774      * @param excludes the excludes pattern, comma separated
1775      * @param includeBasedir true to include the base dir in each String of file
1776      * @return a list of files as String
1777      * @throws IOException io issue
1778      */
1779     public static List<String> getFileNames( File directory, String includes, String excludes, boolean includeBasedir )
1780         throws IOException
1781     {
1782         return getFileNames( directory, includes, excludes, includeBasedir, true );
1783     }
1784 
1785     /**
1786      * Return a list of files as String depending options.
1787      *
1788      * @param directory the directory to scan
1789      * @param includes the includes pattern, comma separated
1790      * @param excludes the excludes pattern, comma separated
1791      * @param includeBasedir true to include the base dir in each String of file
1792      * @param isCaseSensitive true if case sensitive
1793      * @return a list of files as String
1794      * @throws IOException io issue
1795      */
1796     public static List<String> getFileNames( File directory, String includes, String excludes, boolean includeBasedir,
1797                                              boolean isCaseSensitive )
1798         throws IOException
1799     {
1800         return getFileAndDirectoryNames( directory, includes, excludes, includeBasedir, isCaseSensitive, true, false );
1801     }
1802 
1803     /**
1804      * Return a list of directories as String depending options. This method use case sensitive file name.
1805      *
1806      * @param directory the directory to scan
1807      * @param includes the includes pattern, comma separated
1808      * @param excludes the excludes pattern, comma separated
1809      * @param includeBasedir true to include the base dir in each String of file
1810      * @return a list of directories as String
1811      * @throws IOException io issue
1812      */
1813     public static List<String> getDirectoryNames( File directory, String includes, String excludes,
1814                                                   boolean includeBasedir )
1815         throws IOException
1816     {
1817         return getDirectoryNames( directory, includes, excludes, includeBasedir, true );
1818     }
1819 
1820     /**
1821      * Return a list of directories as String depending options.
1822      *
1823      * @param directory the directory to scan
1824      * @param includes the includes pattern, comma separated
1825      * @param excludes the excludes pattern, comma separated
1826      * @param includeBasedir true to include the base dir in each String of file
1827      * @param isCaseSensitive true if case sensitive
1828      * @return a list of directories as String
1829      * @throws IOException io issue
1830      */
1831     public static List<String> getDirectoryNames( File directory, String includes, String excludes,
1832                                                   boolean includeBasedir, boolean isCaseSensitive )
1833         throws IOException
1834     {
1835         return getFileAndDirectoryNames( directory, includes, excludes, includeBasedir, isCaseSensitive, false, true );
1836     }
1837 
1838     /**
1839      * Return a list of files as String depending options.
1840      *
1841      * @param directory the directory to scan
1842      * @param includes the includes pattern, comma separated
1843      * @param excludes the excludes pattern, comma separated
1844      * @param includeBasedir true to include the base dir in each String of file
1845      * @param isCaseSensitive true if case sensitive
1846      * @param getFiles true if get files
1847      * @param getDirectories true if get directories
1848      * @return a list of files as String
1849      * @throws IOException io issue
1850      */
1851     public static List<String> getFileAndDirectoryNames( File directory, String includes, String excludes,
1852                                                          boolean includeBasedir, boolean isCaseSensitive,
1853                                                          boolean getFiles, boolean getDirectories )
1854         throws IOException
1855     {
1856         DirectoryScanner scanner = new DirectoryScanner();
1857 
1858         scanner.setBasedir( directory );
1859 
1860         if ( includes != null )
1861         {
1862             scanner.setIncludes( StringUtils.split( includes, "," ) );
1863         }
1864 
1865         if ( excludes != null )
1866         {
1867             scanner.setExcludes( StringUtils.split( excludes, "," ) );
1868         }
1869 
1870         scanner.setCaseSensitive( isCaseSensitive );
1871 
1872         scanner.scan();
1873 
1874         List<String> list = new ArrayList<String>();
1875 
1876         if ( getFiles )
1877         {
1878             String[] files = scanner.getIncludedFiles();
1879 
1880             for ( String file : files )
1881             {
1882                 if ( includeBasedir )
1883                 {
1884                     list.add( directory + FileUtils.FS + file );
1885                 }
1886                 else
1887                 {
1888                     list.add( file );
1889                 }
1890             }
1891         }
1892 
1893         if ( getDirectories )
1894         {
1895             String[] directories = scanner.getIncludedDirectories();
1896 
1897             for ( String directory1 : directories )
1898             {
1899                 if ( includeBasedir )
1900                 {
1901                     list.add( directory + FileUtils.FS + directory1 );
1902                 }
1903                 else
1904                 {
1905                     list.add( directory1 );
1906                 }
1907             }
1908         }
1909 
1910         return list;
1911     }
1912 
1913     /**
1914      * Copy a directory to an other one.
1915      *
1916      * @param sourceDirectory the source dir
1917      * @param destinationDirectory the target dir
1918      * @throws IOException if any
1919      */
1920     public static void copyDirectory( File sourceDirectory, File destinationDirectory )
1921         throws IOException
1922     {
1923         copyDirectory( sourceDirectory, destinationDirectory, "**", null );
1924     }
1925 
1926     /**
1927      * Copy a directory to an other one.
1928      *
1929      * @param sourceDirectory the source dir
1930      * @param destinationDirectory the target dir
1931      * @param includes include pattern
1932      * @param excludes exclude pattern
1933      * @throws IOException if any
1934      * @see #getFiles(File, String, String)
1935      */
1936     public static void copyDirectory( File sourceDirectory, File destinationDirectory, String includes,
1937                                       String excludes )
1938         throws IOException
1939     {
1940         if ( !sourceDirectory.exists() )
1941         {
1942             return;
1943         }
1944 
1945         List<File> files = getFiles( sourceDirectory, includes, excludes );
1946 
1947         for ( File file : files )
1948         {
1949             copyFileToDirectory( file, destinationDirectory );
1950         }
1951     }
1952 
1953     /**
1954      * <p>Copies a entire directory layout : no files will be copied only directories</p>
1955      * 
1956      * Note:
1957      * <ul>
1958      * <li>It will include empty directories.
1959      * <li>The <code>sourceDirectory</code> must exists.
1960      * </ul>
1961      *
1962      * @param sourceDirectory the source dir
1963      * @param destinationDirectory the target dir
1964      * @param includes include pattern
1965      * @param excludes exclude pattern
1966      * @throws IOException if any
1967      * @since 1.5.7
1968      */
1969     public static void copyDirectoryLayout( File sourceDirectory, File destinationDirectory, String[] includes,
1970                                             String[] excludes )
1971         throws IOException
1972     {
1973         if ( sourceDirectory == null )
1974         {
1975             throw new IOException( "source directory can't be null." );
1976         }
1977 
1978         if ( destinationDirectory == null )
1979         {
1980             throw new IOException( "destination directory can't be null." );
1981         }
1982 
1983         if ( sourceDirectory.equals( destinationDirectory ) )
1984         {
1985             throw new IOException( "source and destination are the same directory." );
1986         }
1987 
1988         if ( !sourceDirectory.exists() )
1989         {
1990             throw new IOException( "Source directory doesn't exists (" + sourceDirectory.getAbsolutePath() + ")." );
1991         }
1992 
1993         DirectoryScanner scanner = new DirectoryScanner();
1994 
1995         scanner.setBasedir( sourceDirectory );
1996 
1997         if ( includes != null && includes.length >= 1 )
1998         {
1999             scanner.setIncludes( includes );
2000         }
2001         else
2002         {
2003             scanner.setIncludes( new String[] { "**" } );
2004         }
2005 
2006         if ( excludes != null && excludes.length >= 1 )
2007         {
2008             scanner.setExcludes( excludes );
2009         }
2010 
2011         scanner.addDefaultExcludes();
2012         scanner.scan();
2013         List<String> includedDirectories = Arrays.asList( scanner.getIncludedDirectories() );
2014 
2015         for ( String name : includedDirectories )
2016         {
2017             File source = new File( sourceDirectory, name );
2018 
2019             if ( source.equals( sourceDirectory ) )
2020             {
2021                 continue;
2022             }
2023 
2024             File destination = new File( destinationDirectory, name );
2025             destination.mkdirs();
2026         }
2027     }
2028 
2029     /**
2030      * <p>Copies a entire directory structure.</p>
2031      * 
2032      * Note:
2033      * <ul>
2034      * <li>It will include empty directories.
2035      * <li>The <code>sourceDirectory</code> must exists.
2036      * </ul>
2037      *
2038      * @param sourceDirectory the source dir
2039      * @param destinationDirectory the target dir
2040      * @throws IOException if any
2041      */
2042     public static void copyDirectoryStructure( File sourceDirectory, File destinationDirectory )
2043         throws IOException
2044     {
2045         copyDirectoryStructure( sourceDirectory, destinationDirectory, destinationDirectory, false );
2046     }
2047 
2048     /**
2049      * <p>Copies an entire directory structure but only source files with timestamp later than the destinations'.</p>
2050      * 
2051      * Note:
2052      * <ul>
2053      * <li>It will include empty directories.
2054      * <li>The <code>sourceDirectory</code> must exists.
2055      * </ul>
2056      *
2057      * @param sourceDirectory the source dir
2058      * @param destinationDirectory the target dir
2059      * @throws IOException if any
2060      */
2061     public static void copyDirectoryStructureIfModified( File sourceDirectory, File destinationDirectory )
2062         throws IOException
2063     {
2064         copyDirectoryStructure( sourceDirectory, destinationDirectory, destinationDirectory, true );
2065     }
2066 
2067     private static void copyDirectoryStructure( File sourceDirectory, File destinationDirectory,
2068                                                 File rootDestinationDirectory, boolean onlyModifiedFiles )
2069         throws IOException
2070     {
2071         if ( sourceDirectory == null )
2072         {
2073             throw new IOException( "source directory can't be null." );
2074         }
2075 
2076         if ( destinationDirectory == null )
2077         {
2078             throw new IOException( "destination directory can't be null." );
2079         }
2080 
2081         if ( sourceDirectory.equals( destinationDirectory ) )
2082         {
2083             throw new IOException( "source and destination are the same directory." );
2084         }
2085 
2086         if ( !sourceDirectory.exists() )
2087         {
2088             throw new IOException( "Source directory doesn't exists (" + sourceDirectory.getAbsolutePath() + ")." );
2089         }
2090 
2091         File[] files = sourceDirectory.listFiles();
2092 
2093         String sourcePath = sourceDirectory.getAbsolutePath();
2094 
2095         for ( File file : files )
2096         {
2097             if ( file.equals( rootDestinationDirectory ) )
2098             {
2099                 // We don't copy the destination directory in itself
2100                 continue;
2101             }
2102 
2103             String dest = file.getAbsolutePath();
2104 
2105             dest = dest.substring( sourcePath.length() + 1 );
2106 
2107             File destination = new File( destinationDirectory, dest );
2108 
2109             if ( file.isFile() )
2110             {
2111                 destination = destination.getParentFile();
2112 
2113                 if ( onlyModifiedFiles )
2114                 {
2115                     copyFileToDirectoryIfModified( file, destination );
2116                 }
2117                 else
2118                 {
2119                     copyFileToDirectory( file, destination );
2120                 }
2121             }
2122             else if ( file.isDirectory() )
2123             {
2124                 if ( !destination.exists() && !destination.mkdirs() )
2125                 {
2126                     throw new IOException( "Could not create destination directory '" + destination.getAbsolutePath()
2127                         + "'." );
2128                 }
2129 
2130                 copyDirectoryStructure( file, destination, rootDestinationDirectory, onlyModifiedFiles );
2131             }
2132             else
2133             {
2134                 throw new IOException( "Unknown file type: " + file.getAbsolutePath() );
2135             }
2136         }
2137     }
2138 
2139     /**
2140      * <p>Renames a file, even if that involves crossing file system boundaries.</p>
2141      * 
2142      * <p>This will remove <code>to</code> (if it exists), ensure that <code>to</code>'s parent directory exists and move
2143      * <code>from</code>, which involves deleting <code>from</code> as well.</p>
2144      *
2145      * @param from the file to move
2146      * @param to the new file name
2147      * @throws IOException if anything bad happens during this process. Note that <code>to</code> may have been deleted
2148      *             already when this happens.
2149      */
2150     public static void rename( File from, File to )
2151         throws IOException
2152     {
2153         if ( to.exists() && !to.delete() )
2154         {
2155             throw new IOException( "Failed to delete " + to + " while trying to rename " + from );
2156         }
2157 
2158         File parent = to.getParentFile();
2159         if ( parent != null && !parent.exists() && !parent.mkdirs() )
2160         {
2161             throw new IOException( "Failed to create directory " + parent + " while trying to rename " + from );
2162         }
2163 
2164         if ( !from.renameTo( to ) )
2165         {
2166             copyFile( from, to );
2167             if ( !from.delete() )
2168             {
2169                 throw new IOException( "Failed to delete " + from + " while trying to rename it." );
2170             }
2171         }
2172     }
2173 
2174     /**
2175      * <p>Create a temporary file in a given directory.</p>
2176      * 
2177      * <p>The file denoted by the returned abstract pathname did not exist before this method was invoked, any subsequent
2178      * invocation of this method will yield a different file name.</p>
2179      * 
2180      * <p>The filename is prefixNNNNNsuffix where NNNN is a random number</p>
2181      * 
2182      * <p>This method is different to {@link File#createTempFile(String, String, File)} of JDK 1.2 as it doesn't create the
2183      * file itself. It uses the location pointed to by java.io.tmpdir when the parentDir attribute is null.</p>
2184      * 
2185      * <p>To delete automatically the file created by this method, use the {@link File#deleteOnExit()} method.</p>
2186      *
2187      * @param prefix prefix before the random number
2188      * @param suffix file extension; include the '.'
2189      * @param parentDir Directory to create the temporary file in <code>-java.io.tmpdir</code> used if not specificed
2190      * @return a File reference to the new temporary file.
2191      */
2192     public static File createTempFile( String prefix, String suffix, File parentDir )
2193     {
2194         File result = null;
2195         String parent = System.getProperty( "java.io.tmpdir" );
2196         if ( parentDir != null )
2197         {
2198             parent = parentDir.getPath();
2199         }
2200         DecimalFormat fmt = new DecimalFormat( "#####" );
2201         SecureRandom secureRandom = new SecureRandom();
2202         long secureInitializer = secureRandom.nextLong();
2203         Random rand = new Random( secureInitializer + Runtime.getRuntime().freeMemory() );
2204         synchronized ( rand )
2205         {
2206             do
2207             {
2208                 result = new File( parent, prefix + fmt.format( Math.abs( rand.nextInt() ) ) + suffix );
2209             }
2210             while ( result.exists() );
2211         }
2212 
2213         return result;
2214     }
2215 
2216     /**
2217      * <b>If wrappers is null or empty, the file will be copy only if {@code to.lastModified() < from.lastModified()}</b>
2218      *
2219      * @param from the file to copy
2220      * @param to the destination file
2221      * @param encoding the file output encoding (only if wrappers is not empty)
2222      * @param wrappers array of {@link FilterWrapper}
2223      * @throws IOException if an IO error occurs during copying or filtering
2224      */
2225     public static void copyFile( File from, File to, String encoding, FilterWrapper[] wrappers )
2226         throws IOException
2227     {
2228         copyFile( from, to, encoding, wrappers, false );
2229     }
2230 
2231     public static abstract class FilterWrapper
2232     {
2233         public abstract Reader getReader( Reader fileReader );
2234     }
2235 
2236     /**
2237      * <b>If wrappers is null or empty, the file will be copy only if {@code to.lastModified() < from.lastModified()}, if overwrite is true</b>
2238      *
2239      * @param from the file to copy
2240      * @param to the destination file
2241      * @param encoding the file output encoding (only if wrappers is not empty)
2242      * @param wrappers array of {@link FilterWrapper}
2243      * @param overwrite if true and wrappers is null or empty, the file will be copied even if {@code to.lastModified() < from.lastModified()}
2244      * @throws IOException if an IO error occurs during copying or filtering
2245      * @since 1.5.2
2246      */
2247     public static void copyFile( File from, File to, String encoding, FilterWrapper[] wrappers, boolean overwrite )
2248         throws IOException
2249     {
2250         if ( wrappers != null && wrappers.length > 0 )
2251         {
2252             // buffer so it isn't reading a byte at a time!
2253             Reader fileReader = null;
2254             Writer fileWriter = null;
2255             try
2256             {
2257                 if ( encoding == null || encoding.length() < 1 )
2258                 {
2259                     fileReader = Files.newBufferedReader( from.toPath() );
2260                     fileWriter = Files.newBufferedWriter( to.toPath() );
2261                 }
2262                 else
2263                 {
2264                     OutputStream outstream = Files.newOutputStream( to.toPath() );
2265 
2266                     fileReader = Files.newBufferedReader( from.toPath(), Charset.forName( encoding ) );
2267 
2268                     fileWriter = new OutputStreamWriter( outstream, encoding );
2269                 }
2270 
2271                 Reader reader = fileReader;
2272                 for ( FilterWrapper wrapper : wrappers )
2273                 {
2274                     reader = wrapper.getReader( reader );
2275                 }
2276 
2277                 IOUtil.copy( reader, fileWriter );
2278                 fileWriter.close();
2279                 fileWriter = null;
2280                 fileReader.close();
2281                 fileReader = null;
2282             }
2283             finally
2284             {
2285                 IOUtil.close( fileReader );
2286                 IOUtil.close( fileWriter );
2287             }
2288         }
2289         else
2290         {
2291             if ( isSourceNewerThanDestination( from, to ) || overwrite )
2292             {
2293                 copyFile( from, to );
2294             }
2295         }
2296     }
2297 
2298     private static boolean isSourceNewerThanDestination( File source, File destination ) {
2299         return ( destination.lastModified() == 0L && source.lastModified() == 0L ) || destination.lastModified() < source.lastModified();
2300     }
2301 
2302     /**
2303      * Note: the file content is read with platform encoding
2304      *
2305      * @param file the file
2306      * @return a List containing every every line not starting with # and not empty
2307      * @throws IOException if any
2308      */
2309     public static List<String> loadFile( File file )
2310         throws IOException
2311     {
2312         final List<String> lines = new ArrayList<String>();
2313 
2314         if ( file.exists() )
2315         {
2316             try ( BufferedReader reader = Files.newBufferedReader( file.toPath() ) )
2317             {
2318                 for ( String line = reader.readLine(); line != null; line = reader.readLine() )
2319                 {
2320                     line = line.trim();
2321 
2322                     if ( !line.startsWith( "#" ) && line.length() != 0 )
2323                     {
2324                         lines.add( line );
2325                     }
2326                 }
2327             }
2328         }
2329 
2330         return lines;
2331     }
2332 
2333     /**
2334      * For Windows OS, check if the file name contains any of the following characters:
2335      * <code>":", "*", "?", "\"", "&lt;", "&gt;", "|"</code>
2336      *
2337      * @param f not null file
2338      * @return <code>false</code> if the file path contains any of forbidden Windows characters, <code>true</code> if
2339      *         the Os is not Windows or if the file path respect the Windows constraints.
2340      * @see #INVALID_CHARACTERS_FOR_WINDOWS_FILE_NAME
2341      * @since 1.5.2
2342      */
2343     public static boolean isValidWindowsFileName( File f )
2344     {
2345         if ( Os.isFamily( Os.FAMILY_WINDOWS ) )
2346         {
2347             if ( StringUtils.indexOfAny( f.getName(), INVALID_CHARACTERS_FOR_WINDOWS_FILE_NAME ) != -1 )
2348             {
2349                 return false;
2350             }
2351 
2352             File parentFile = f.getParentFile();
2353             if ( parentFile != null )
2354             {
2355                 return isValidWindowsFileName( parentFile );
2356             }
2357         }
2358 
2359         return true;
2360     }
2361 }