001package votorola.g.io; // Copyright 2004-2007, 2009, 2011, Michael Allan.  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE.
002
003import java.io.*;
004import java.nio.charset.*;
005import java.util.logging.*;
006import java.util.regex.*;
007import votorola.g.lang.*;
008import votorola.g.logging.*;
009
010
011/** File utilities.
012  */
013public @ThreadSafe final class FileX
014{
015
016    private FileX() {}
017
018
019
020    /** Appends the entire file to the specified appendable.
021      *
022      *     @return the same appendable.
023      */
024    public static Appendable appendTo( final Appendable a, final File file,
025      final Charset fileCharset ) throws IOException
026    {
027        final BufferedReader in = new BufferedReader(
028          new InputStreamReader( new FileInputStream(file), fileCharset ));
029        try{ return ReaderX.appendTo( a, in ); }
030        finally{ in.close(); }
031    }
032
033
034
035    /** A pattern to split a filename (or path) into two groups: body and dot-extension.
036      * The body group (1) may include separators '/'.  It will be neither empty nor null.
037      *
038      * <p>The extension group (2) will either include the preceding dot '.', or it will
039      * be empty.  It may comprise a single dot.  It will not be null.</p>
040      */
041    public static final Pattern BODY_DOTX_PATTERN = Pattern.compile( "^(.+?)((?:\\.[^.]*)?)$" );
042
043
044
045    /** Copies a file or directory to a new file.  No checking is done to prevent
046      * self-copying.
047      *
048      *     @param target pathname to create as copy.  It will be overwritten if it
049      *       already exists.
050      *     @param source file or directory to copy.  Directory contents are recursively
051      *       copied using {@link #copyTo(File,File) copyTo}.
052      */
053    public static void copyAs( final File target, final File source ) throws IOException
054    {
055        copyAs( target, source, FileFilterX.TRANSPARENT );
056    }
057
058
059
060    /** Copies a file or directory to a new file.  No checking is done to prevent
061      * self-copying.
062      *
063      *     @param target pathname to create as copy.  It will be overwritten if it
064      *       already exists.
065      *     @param source file or directory to copy.  Directory contents are recursively
066      *       copied using {@link #copyTo(File,File,FileFilter) copyTo}.
067      *     @param fileFilter the filter to use in case the source is a directory.  The
068      *       filter is applied to the content of the source directory, and recursively to
069      *       the content of any sub-directories that themselves pass the filter.
070      */
071    public static void copyAs( File target, File source, FileFilter fileFilter ) throws IOException
072    {
073        if( source.isDirectory() )
074        {
075          // First the directory itself.
076          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
077            if( target.isDirectory() ) // clean it out, in case not empty:
078            {
079                if( !deleteRecursiveFrom(target) ) throw new IOException( "unable to delete directory contents of " + target );
080            }
081            else if( !target.mkdir() ) throw new IOException( "unable to create directory " + target );
082
083          // Then its contents, if any.
084          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
085            File[] subSourceArray = source.listFiles( fileFilter );
086            for( int i=0; i<subSourceArray.length; ++i )
087            {
088                copyTo( target, subSourceArray[i] );
089            }
090        }
091     // else if( source.isFile() )
092        else // file (perhaps not a normal one, though)
093        {
094            InputStream input = new BufferedInputStream( new FileInputStream( source ));
095            try
096            {
097                OutputStream output = new BufferedOutputStream( new FileOutputStream( target ));
098                try
099                {
100                    for( ;; ) { int i = input.read(); if( i == -1 ) break; output.write( i ); }
101                }
102                finally{ output.close(); }
103            }
104            finally{ input.close(); }
105        }
106    }
107
108
109
110    /** Copies a file or directory to a directory.
111      *
112      *     @param targetDirectory in which to make copy.  It must already exist.
113      *     @param source file or directory to copy.  A directory is recursively copied,
114      *       overwriting any existing file or directory of the same name.
115      */
116    public static void copyTo( File targetDirectory, File source ) throws IOException
117    {
118        copyTo( targetDirectory, source, FileFilterX.TRANSPARENT );
119    }
120
121
122
123    /** Copies a file or directory to a directory.
124      *
125      *     @param targetDirectory the directory in which to make the copy.  It must
126      *       already exist.
127      *     @param source file or directory to copy.  A directory is recursively copied,
128      *       overwriting any existing file or directory of the same name.
129      *     @param fileFilter the filter to use in case the source is a directory.  The
130      *       filter is applied to the content of the source directory, and recursively to
131      *       the content of any sub-directories that themselves pass the filter.
132      */
133    public static void copyTo( File targetDirectory, File source, FileFilter fileFilter )
134      throws IOException
135    {
136        File target = new File( targetDirectory, source.getName() );
137        copyAs( target, source, fileFilter );
138    }
139
140
141
142    /** Atomically creates a new, empty directory if and only if the directory does not
143      * yet exist.  Similar to createNewFile(), but creates a directory.  Also creates any
144      * necessary parent directories.
145      *
146      *     @param directory the directory to create, if it does not already exist.
147      *     @return true if the directory was created, false if it already existed.
148      *
149      *     @throws IOException if the directory did not exist, and could not be created.
150      */
151    public static boolean createNewDirectory( File directory ) throws IOException // 'ensureDirectory' would be better, except 'createNewDirectory' is consistent with File's 'createNewFile'
152    {
153        if( directory.isDirectory() ) return false;
154
155        if( directory.mkdirs() )  return true;
156
157        if( directory.isDirectory() ) return false; // checking once again, in case failure was caused by another thread that created the directory
158
159        throw new IOException( "unable to create directory: " + directory );
160    }
161
162
163
164 // /** The same as {@linkplain File#delete() delete}() except it throws an exception if
165 //   * it fails.
166 //   */
167 // public static void deleteSure( final File file ) throws IOException
168 // {
169 //     if( !file.delete() ) throw new IOException( "unable to delete file: " + file );
170 // }
171
172
173
174    /** The same as {@linkplain File#delete() delete}() except it works with non-empty
175      * directories.
176      *
177      *     @return true if entirely deleted, false otherwise.
178      *
179      *     @throws SecurityException if the application does not have permission
180      *       to delete the fileOrDirectory.
181      */
182    public static boolean deleteRecursive( final File fileOrDirectory )
183    {
184        if( fileOrDirectory.isFile() ) return fileOrDirectory.delete();
185        else return deleteRecursiveFrom(fileOrDirectory) && fileOrDirectory.delete();
186    }
187
188
189
190    /** The same as {@link #deleteRecursive(File) deleteRecursive}(), but it works only on
191      * the contents of the specified directory.
192      *
193      *     @param directory the directory whose contents to delete.
194      *     @return true if deleted, false otherwise.
195      *
196      *     @throws SecurityException if the application does not have permission
197      *       to delete the directory.
198      */
199    public static boolean deleteRecursiveFrom( final File directory )
200    {
201        boolean deletedAll = true; // till proven false
202        for( File file: directory.listFiles() ) deletedAll = deletedAll && deleteRecursive( file );
203        return deletedAll;
204    }
205
206
207
208    /** The same as {@linkplain #deleteRecursive(File) deleteRecursive}() except it throws
209      * an exception if it fails.
210      *
211      *     @throws SecurityException if the application does not have permission
212      *       to delete the fileOrDirectory.
213      */
214    public static void deleteRecursiveSure( final File fileOrDirectory ) throws IOException
215    {
216        if( !deleteRecursive( fileOrDirectory ))
217        {
218            throw new IOException( "unable to recursively delete file or directory: "
219              + fileOrDirectory );
220        }
221    }
222
223
224
225    /** Ensures that the specified directory is created, including any necessary but
226      * nonexistent parent directories.
227      *
228      *     @param directory the directory to ensure.  If it is null, this method is a
229      *       no-op.
230      *
231      *     @see File#mkdirs()
232      */
233    public static void ensuredirs( final File directory ) throws IOException
234    {
235        if( directory == null || directory.isDirectory() ) return;
236
237        if( !directory.mkdirs() ) throw new IOException( "unable to create directory, or one of its parent directories: " + directory );
238    }
239
240
241
242    /** The same as {@linkplain File#list() list}(), but it never returns null.  It
243      * returns an empty array instead.
244      */
245    public static String[] listNoNull( final File file ) { return listNoNull( file, null );}
246
247
248
249    /** The same as {@linkplain File#list(FilenameFilter) list}(filter), but it never
250      * returns null.  It returns an empty array instead.
251      */
252    public static String[] listNoNull( final File file, final FilenameFilter filter )
253    {
254        String[] pathArray = file.list( filter );
255        return pathArray == null? new String[] {}: pathArray;
256    }
257
258
259
260    /** The same as {@linkplain File#listFiles() listFiles}(), but it never returns null.
261      * It returns an empty array instead.
262      */
263    public static File[] listFilesNoNull( final File file ) { return listFilesNoNull( file, null ); }
264
265
266
267    /** The same as {@linkplain File#listFiles(FileFilter) listFiles}(filter), but it
268      * never returns null.  It returns an empty array instead.
269      */
270    public static File[] listFilesNoNull( final File file, final FileFilter filter )
271    {
272        File[] fileArray = file.listFiles( filter );
273        return fileArray == null? new File[] {}: fileArray;
274    }
275
276
277
278    /** Deserializes an object from a file.
279      *
280      *     @see #writeObject(Object,File)
281      */
282    public static Object readObject( final File file ) throws ClassNotFoundException, IOException
283    {
284        final ObjectInputStream in = new ObjectInputStream( new BufferedInputStream(
285          new FileInputStream( file )));
286        try{ return in.readObject(); }
287        finally{ in.close(); }
288    }
289
290
291
292 // /** Tries hard to rename a file, despite bug 6213298.
293 //   *
294 //   *     @return whether the rename succeeded.
295 //   */
296 // public static boolean renameFrom( final File oldFile, final File newFile )
297 // {
298 //     // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6213298
299 //     // Submitter's note follows.
300 //     //
301 //     // "HACK - I should just be able to call renameTo() here and return its result. In
302 //     // fact, I used to do just that and this method always worked fine. Now with this
303 //     // new version of Java (1.5.0), rename (and other file methods) sometimes don't
304 //     // work on the first try. This is because file objects that have been closed are
305 //     // hung onto, pending garbage collection. By suggesting garbage collection, the
306 //     // next time, the renameTo() usually (but not always) works."
307 //
308 //     if( oldFile.renameTo( newFile )) return true;
309 //
310 //     System.gc();
311 //     ThreadX.trySleep( 50/*ms*/ );
312 //     if( oldFile.renameTo( newFile )) return true;
313 //
314 //     System.gc();
315 //     ThreadX.trySleep( 450/*ms*/ );
316 //     if( oldFile.renameTo( newFile )) return true;
317 //
318 //     System.gc();
319 //     ThreadX.trySleep( 1500/*ms*/ );
320 //     if( oldFile.renameTo( newFile )) return true;
321 //
322 //     return false;
323 // }
324 //
325 /// Christian says it fails regardless across file systems, but will be fixed in JDK 1.7.
326 /// I think I've seen that failure, too.  Instead use renameFromDefaultToMv.
327
328
329
330    /** Renames a file, defaulting to a Unix <code>mv</code> command if it fails.
331      *
332      *     @throws IOException if the file cannot be renamed.
333      */
334    public static void renameFromDefaultToMv( final File oldFile, final File newFile )
335      throws IOException
336    {
337        if( oldFile.renameTo( newFile )) return;
338
339        final ProcessBuilder pB = new ProcessBuilder(
340          "/bin/mv", oldFile.getPath(), newFile.getPath() );
341        logger.config( "Java rename failed, defaulting to OS call: " + pB.command() );
342        int exitValue = ProcessX.waitForWithoutInterrupt( pB.start() );
343        if( exitValue != 0 ) throw new IOException( "exit value of " + exitValue + " from process: " + pB.command() );
344    }
345
346
347
348 // /** First tries renameFrom(), failing that it makes a copy.  Does not delete the old
349 //   * file.
350 //   *
351 //   *     @return true if it had to resort to copying; false if the rename worked.
352 //   */
353 // public static boolean renameFromDefaultsToCopy( final File oldFile, final File newFile ) throws IOException
354 // {
355 //     if( oldFile.renameTo( newFile )) return true;
356 //     if( renameFrom( oldFile, newFile )) return false;
357 //
358 //     ThreadX.trySleep( 500/*ms*/ );
359 //     logger.fine( "Java rename failed, defaulting to copy: " + oldFile );
360 //     copyAs( newFile, oldFile );
361 //     return true;
362 // }
363
364
365
366 // /** @return safe name for a file, based on originalName supplied. This implementation checks
367 //   *   ntent only (not length). If all characters are recognized, then the originalName
368 //   *   reference is returned. Otherwise returns a copy of the originalName with any
369 //   *   unrecognized characters converted to underscores. Recognizes only alphanumeric
370 //   *   characters and underscores. All others, including the period '.' character, are
371 //   *   converted to underscores.
372 //   */ @Deprecated // in favour of ~.io.FileNameEncoder
373 // public static String safeName( String originalName )
374 // {
375 //     char[] array = originalName.toCharArray();
376 //     boolean arrayChanged = false; // till proven otherwise
377 //     for( int i=0; i<array.length; ++i )
378 //     {
379 //         char c = array[i];
380 //         if(  c!='_' && !Character.isLetterOrDigit(c) )
381 //         {
382 //             array[i] = '_';
383 //             arrayChanged = true;
384 //         }
385 //     }
386 //     if( arrayChanged )
387 //         return new String( array );
388 //     else
389 //         return originalName;
390 // }
391
392
393
394    /** Creates a new symbolic link at <code>linkPath</code>, pointing to
395      * <code>targetPath</code>.  This is likely to fail on non-Unix platforms.  It is
396      * equivalent to the Unix command:
397      *
398      * <pre class='vspace indent'>ln --force --no-dereference --symbolic <var>targetPath</var> <var>linkPath</var></pre>
399      *
400      *     @param targetPath the path to which the link file will point.
401      *       If the path is relative, then it is anchored at the link file.
402      *     @param linkPath the path of the link file to create.
403      *
404      *     @throws IOException if the attempt fails.
405      */
406    public static void symlink( final String targetPath, final String linkPath ) throws IOException
407    {
408        final File link = new File( linkPath );
409        if( link.exists() && !link.delete() ) throw new IOException( "unable to delete symbolic link, prior to relinking: " + linkPath );
410          // If it is not deleted here in the VM, then "new
411          // File(linkPath).getCanonicalFile()" will see the old target for about 30 s.
412          // Java (1.6.0.07) is somehow caching it.
413
414        final ProcessBuilder pB = new ProcessBuilder(
415     //   "/bin/sh", "-c", "ln --force --no-dereference --symbolic " + targetPath + " " + linkPath );
416          "/bin/ln", "--force", "--no-dereference", "--symbolic", targetPath, linkPath );
417        logger.config( "calling out to OS: " + pB.command() );
418        int exitValue = ProcessX.waitForWithoutInterrupt( pB.start() );
419        if( exitValue != 0 ) throw new IOException( "exit value of " + exitValue + " from process: " + pB.command() );
420    }
421
422
423
424 // /** Returns a new instance of file system's temporary directory,
425 //   * as defined by System property 'java.io.tmpdir'.
426 //   */
427 // public static File tempDirectory()
428 // {
429 //     return new File( System.getProperty( "java.io.tmpdir" ));
430 // }
431
432
433
434    /** Tests whether or not the file system is case sensitive, and returns true iff it
435      * is.
436      */
437    public static boolean testsCaseSensitive()
438    {
439        File lower = new File( "a" );
440        File upper = new File( "A" );
441        return !lower.equals( upper );
442    }
443
444
445
446    /** Traverses the file and its ancestors; first from the bottom up, then from the top
447      * down.  Upward traversal commences with the bottom-most file, and stops when all
448      * ancestors are exhausted, or when the upFilter returns false.  Downward traversal
449      * commences with the topmost file, and stops on returning to the bottom-most file,
450      * or when the downFilter returns false.
451      *
452      *     @param bottomFile the bottom-most file.
453      *     @param upFilter the upward traversal filter.  It returns true
454      *       if upward traversal is to continue; false if it is to stop.
455      *     @param downFilter the downward traversal filter.  It returns true
456      *       if downward traversal is to continue; false if it is to stop.
457      *
458      *     @return the last result from the downFilter.
459      */
460    public static boolean traverse( final File bottomFile, final FileFilter upFilter,
461      final FileFilter downFilter )
462    {
463        boolean toContinue = true; // so far
464        if( upFilter.accept( bottomFile ))
465        {
466            final File dir = bottomFile.getParentFile();
467            if( dir != null ) toContinue = traverse( dir, upFilter, downFilter );
468        }
469
470        if( toContinue ) toContinue = downFilter.accept( bottomFile );
471        return toContinue;
472    }
473
474
475
476    /** Traverses the file and its descendants from the top down, beginning with the
477      * topmost file itself.  Traversal stops when all descendants are exhausted, or when
478      * the filter returns false.
479      *
480      *     @param topFile the top-most file.
481      *     @param filter the traversal filter.  It returns true if traversal is to
482      *       continue; false if it is to stop.
483      *
484      *     @return the last result from the filter.
485      */
486    public static boolean traverseDown( final File topFile, final FileFilter filter )
487    {
488        boolean toContinue = true; // so far
489        if( !filter.accept( topFile )) toContinue = false;
490        else if( topFile.isDirectory() )
491        {
492            for( final File child: topFile.listFiles() )
493            {
494                if( traverseDown( child, filter )) continue;
495
496                toContinue = false;
497                break;
498            }
499        }
500
501        return toContinue;
502    }
503
504
505
506    /** Traverses the file and its ancestors from the bottom up, beginning with the
507      * bottom-most file itself.  Traversal stops when all ancestors are exhausted, or
508      * when the filter returns false.
509      *
510      *     @param bottomFile the bottom-most file.
511      *     @param filter the traversal filter.  It returns true if traversal is to
512      *       continue; false if it is to stop.
513      */
514    public static void traverseUp( final File bottomFile, final FileFilter filter )
515    {
516        if( filter.accept( bottomFile ))
517        {
518            final File dir = bottomFile.getParentFile();
519            if( dir != null ) traverseUp( dir, filter );
520        }
521    }
522
523
524
525    /** Serializes an object to a file.
526      *
527      *     @see #readObject(File)
528      */
529    public static void writeObject( final Object object, final File file ) throws IOException
530    {
531        final ObjectOutputStream out = new ObjectOutputStream( new BufferedOutputStream(
532          new FileOutputStream( file )));
533        try{ out.writeObject( object ); }
534        finally{ out.close(); }
535    }
536
537
538
539//// P r i v a t e ///////////////////////////////////////////////////////////////////////
540
541
542    private static final Logger logger = LoggerX.i( FileX.class );
543
544
545
546}