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}