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