001 /* 002 * Copyright (C) 2007 The Guava Authors 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017 package com.google.common.io; 018 019 import static com.google.common.base.Preconditions.checkNotNull; 020 021 import com.google.common.annotations.Beta; 022 import com.google.common.base.Joiner; 023 import com.google.common.base.Preconditions; 024 import com.google.common.base.Splitter; 025 026 import java.io.BufferedReader; 027 import java.io.BufferedWriter; 028 import java.io.Closeable; 029 import java.io.File; 030 import java.io.FileInputStream; 031 import java.io.FileNotFoundException; 032 import java.io.FileOutputStream; 033 import java.io.IOException; 034 import java.io.InputStream; 035 import java.io.InputStreamReader; 036 import java.io.OutputStream; 037 import java.io.OutputStreamWriter; 038 import java.io.RandomAccessFile; 039 import java.nio.MappedByteBuffer; 040 import java.nio.channels.FileChannel; 041 import java.nio.channels.FileChannel.MapMode; 042 import java.nio.charset.Charset; 043 import java.security.MessageDigest; 044 import java.util.ArrayList; 045 import java.util.List; 046 import java.util.zip.Checksum; 047 048 /** 049 * Provides utility methods for working with files. 050 * 051 * <p>All method parameters must be non-null unless documented otherwise. 052 * 053 * @author Chris Nokleberg 054 * @since 1.0 055 */ 056 @Beta 057 public final class Files { 058 059 /** Maximum loop count when creating temp directories. */ 060 private static final int TEMP_DIR_ATTEMPTS = 10000; 061 062 private Files() {} 063 064 /** 065 * Returns a buffered reader that reads from a file using the given 066 * character set. 067 * 068 * @param file the file to read from 069 * @param charset the character set used when writing the file 070 * @return the buffered reader 071 */ 072 public static BufferedReader newReader(File file, Charset charset) 073 throws FileNotFoundException { 074 return new BufferedReader( 075 new InputStreamReader(new FileInputStream(file), charset)); 076 } 077 078 /** 079 * Returns a buffered writer that writes to a file using the given 080 * character set. 081 * 082 * @param file the file to write to 083 * @param charset the character set used when writing the file 084 * @return the buffered writer 085 */ 086 public static BufferedWriter newWriter(File file, Charset charset) 087 throws FileNotFoundException { 088 return new BufferedWriter( 089 new OutputStreamWriter(new FileOutputStream(file), charset)); 090 } 091 092 /** 093 * Returns a factory that will supply instances of {@link FileInputStream} 094 * that read from a file. 095 * 096 * @param file the file to read from 097 * @return the factory 098 */ 099 public static InputSupplier<FileInputStream> newInputStreamSupplier( 100 final File file) { 101 Preconditions.checkNotNull(file); 102 return new InputSupplier<FileInputStream>() { 103 @Override 104 public FileInputStream getInput() throws IOException { 105 return new FileInputStream(file); 106 } 107 }; 108 } 109 110 /** 111 * Returns a factory that will supply instances of {@link FileOutputStream} 112 * that write to a file. 113 * 114 * @param file the file to write to 115 * @return the factory 116 */ 117 public static OutputSupplier<FileOutputStream> newOutputStreamSupplier( 118 File file) { 119 return newOutputStreamSupplier(file, false); 120 } 121 122 /** 123 * Returns a factory that will supply instances of {@link FileOutputStream} 124 * that write to or append to a file. 125 * 126 * @param file the file to write to 127 * @param append if true, the encoded characters will be appended to the file; 128 * otherwise the file is overwritten 129 * @return the factory 130 */ 131 public static OutputSupplier<FileOutputStream> newOutputStreamSupplier( 132 final File file, final boolean append) { 133 Preconditions.checkNotNull(file); 134 return new OutputSupplier<FileOutputStream>() { 135 @Override 136 public FileOutputStream getOutput() throws IOException { 137 return new FileOutputStream(file, append); 138 } 139 }; 140 } 141 142 /** 143 * Returns a factory that will supply instances of 144 * {@link InputStreamReader} that read a file using the given character set. 145 * 146 * @param file the file to read from 147 * @param charset the character set used when reading the file 148 * @return the factory 149 */ 150 public static InputSupplier<InputStreamReader> newReaderSupplier(File file, 151 Charset charset) { 152 return CharStreams.newReaderSupplier(newInputStreamSupplier(file), charset); 153 } 154 155 /** 156 * Returns a factory that will supply instances of {@link OutputStreamWriter} 157 * that write to a file using the given character set. 158 * 159 * @param file the file to write to 160 * @param charset the character set used when writing the file 161 * @return the factory 162 */ 163 public static OutputSupplier<OutputStreamWriter> newWriterSupplier(File file, 164 Charset charset) { 165 return newWriterSupplier(file, charset, false); 166 } 167 168 /** 169 * Returns a factory that will supply instances of {@link OutputStreamWriter} 170 * that write to or append to a file using the given character set. 171 * 172 * @param file the file to write to 173 * @param charset the character set used when writing the file 174 * @param append if true, the encoded characters will be appended to the file; 175 * otherwise the file is overwritten 176 * @return the factory 177 */ 178 public static OutputSupplier<OutputStreamWriter> newWriterSupplier(File file, 179 Charset charset, boolean append) { 180 return CharStreams.newWriterSupplier(newOutputStreamSupplier(file, append), 181 charset); 182 } 183 184 /** 185 * Reads all bytes from a file into a byte array. 186 * 187 * @param file the file to read from 188 * @return a byte array containing all the bytes from file 189 * @throws IllegalArgumentException if the file is bigger than the largest 190 * possible byte array (2^31 - 1) 191 * @throws IOException if an I/O error occurs 192 */ 193 public static byte[] toByteArray(File file) throws IOException { 194 Preconditions.checkArgument(file.length() <= Integer.MAX_VALUE); 195 if (file.length() == 0) { 196 // Some special files are length 0 but have content nonetheless. 197 return ByteStreams.toByteArray(newInputStreamSupplier(file)); 198 } else { 199 // Avoid an extra allocation and copy. 200 byte[] b = new byte[(int) file.length()]; 201 boolean threw = true; 202 InputStream in = new FileInputStream(file); 203 try { 204 ByteStreams.readFully(in, b); 205 threw = false; 206 } finally { 207 Closeables.close(in, threw); 208 } 209 return b; 210 } 211 } 212 213 /** 214 * Reads all characters from a file into a {@link String}, using the given 215 * character set. 216 * 217 * @param file the file to read from 218 * @param charset the character set used when reading the file 219 * @return a string containing all the characters from the file 220 * @throws IOException if an I/O error occurs 221 */ 222 public static String toString(File file, Charset charset) throws IOException { 223 return new String(toByteArray(file), charset.name()); 224 } 225 226 /** 227 * Copies to a file all bytes from an {@link InputStream} supplied by a 228 * factory. 229 * 230 * @param from the input factory 231 * @param to the destination file 232 * @throws IOException if an I/O error occurs 233 */ 234 public static void copy(InputSupplier<? extends InputStream> from, File to) 235 throws IOException { 236 ByteStreams.copy(from, newOutputStreamSupplier(to)); 237 } 238 239 /** 240 * Overwrites a file with the contents of a byte array. 241 * 242 * @param from the bytes to write 243 * @param to the destination file 244 * @throws IOException if an I/O error occurs 245 */ 246 public static void write(byte[] from, File to) throws IOException { 247 ByteStreams.write(from, newOutputStreamSupplier(to)); 248 } 249 250 /** 251 * Copies all bytes from a file to an {@link OutputStream} supplied by 252 * a factory. 253 * 254 * @param from the source file 255 * @param to the output factory 256 * @throws IOException if an I/O error occurs 257 */ 258 public static void copy(File from, OutputSupplier<? extends OutputStream> to) 259 throws IOException { 260 ByteStreams.copy(newInputStreamSupplier(from), to); 261 } 262 263 /** 264 * Copies all bytes from a file to an output stream. 265 * 266 * @param from the source file 267 * @param to the output stream 268 * @throws IOException if an I/O error occurs 269 */ 270 public static void copy(File from, OutputStream to) throws IOException { 271 ByteStreams.copy(newInputStreamSupplier(from), to); 272 } 273 274 /** 275 * Copies all the bytes from one file to another. 276 *. 277 * @param from the source file 278 * @param to the destination file 279 * @throws IOException if an I/O error occurs 280 * @throws IllegalArgumentException if {@code from.equals(to)} 281 */ 282 public static void copy(File from, File to) throws IOException { 283 Preconditions.checkArgument(!from.equals(to), 284 "Source %s and destination %s must be different", from, to); 285 copy(newInputStreamSupplier(from), to); 286 } 287 288 /** 289 * Copies to a file all characters from a {@link Readable} and 290 * {@link Closeable} object supplied by a factory, using the given 291 * character set. 292 * 293 * @param from the readable supplier 294 * @param to the destination file 295 * @param charset the character set used when writing the file 296 * @throws IOException if an I/O error occurs 297 */ 298 public static <R extends Readable & Closeable> void copy( 299 InputSupplier<R> from, File to, Charset charset) throws IOException { 300 CharStreams.copy(from, newWriterSupplier(to, charset)); 301 } 302 303 /** 304 * Writes a character sequence (such as a string) to a file using the given 305 * character set. 306 * 307 * @param from the character sequence to write 308 * @param to the destination file 309 * @param charset the character set used when writing the file 310 * @throws IOException if an I/O error occurs 311 */ 312 public static void write(CharSequence from, File to, Charset charset) 313 throws IOException { 314 write(from, to, charset, false); 315 } 316 317 /** 318 * Appends a character sequence (such as a string) to a file using the given 319 * character set. 320 * 321 * @param from the character sequence to append 322 * @param to the destination file 323 * @param charset the character set used when writing the file 324 * @throws IOException if an I/O error occurs 325 */ 326 public static void append(CharSequence from, File to, Charset charset) 327 throws IOException { 328 write(from, to, charset, true); 329 } 330 331 /** 332 * Private helper method. Writes a character sequence to a file, 333 * optionally appending. 334 * 335 * @param from the character sequence to append 336 * @param to the destination file 337 * @param charset the character set used when writing the file 338 * @param append true to append, false to overwrite 339 * @throws IOException if an I/O error occurs 340 */ 341 private static void write(CharSequence from, File to, Charset charset, 342 boolean append) throws IOException { 343 CharStreams.write(from, newWriterSupplier(to, charset, append)); 344 } 345 346 /** 347 * Copies all characters from a file to a {@link Appendable} & 348 * {@link Closeable} object supplied by a factory, using the given 349 * character set. 350 * 351 * @param from the source file 352 * @param charset the character set used when reading the file 353 * @param to the appendable supplier 354 * @throws IOException if an I/O error occurs 355 */ 356 public static <W extends Appendable & Closeable> void copy(File from, 357 Charset charset, OutputSupplier<W> to) throws IOException { 358 CharStreams.copy(newReaderSupplier(from, charset), to); 359 } 360 361 /** 362 * Copies all characters from a file to an appendable object, 363 * using the given character set. 364 * 365 * @param from the source file 366 * @param charset the character set used when reading the file 367 * @param to the appendable object 368 * @throws IOException if an I/O error occurs 369 */ 370 public static void copy(File from, Charset charset, Appendable to) 371 throws IOException { 372 CharStreams.copy(newReaderSupplier(from, charset), to); 373 } 374 375 /** 376 * Returns true if the files contains the same bytes. 377 * 378 * @throws IOException if an I/O error occurs 379 */ 380 public static boolean equal(File file1, File file2) throws IOException { 381 if (file1 == file2 || file1.equals(file2)) { 382 return true; 383 } 384 385 /* 386 * Some operating systems may return zero as the length for files 387 * denoting system-dependent entities such as devices or pipes, in 388 * which case we must fall back on comparing the bytes directly. 389 */ 390 long len1 = file1.length(); 391 long len2 = file2.length(); 392 if (len1 != 0 && len2 != 0 && len1 != len2) { 393 return false; 394 } 395 return ByteStreams.equal(newInputStreamSupplier(file1), 396 newInputStreamSupplier(file2)); 397 } 398 399 /** 400 * Atomically creates a new directory somewhere beneath the system's 401 * temporary directory (as defined by the {@code java.io.tmpdir} system 402 * property), and returns its name. 403 * 404 * <p>Use this method instead of {@link File#createTempFile(String, String)} 405 * when you wish to create a directory, not a regular file. A common pitfall 406 * is to call {@code createTempFile}, delete the file and create a 407 * directory in its place, but this leads a race condition which can be 408 * exploited to create security vulnerabilities, especially when executable 409 * files are to be written into the directory. 410 * 411 * <p>This method assumes that the temporary volume is writable, has free 412 * inodes and free blocks, and that it will not be called thousands of times 413 * per second. 414 * 415 * @return the newly-created directory 416 * @throws IllegalStateException if the directory could not be created 417 */ 418 public static File createTempDir() { 419 File baseDir = new File(System.getProperty("java.io.tmpdir")); 420 String baseName = System.currentTimeMillis() + "-"; 421 422 for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) { 423 File tempDir = new File(baseDir, baseName + counter); 424 if (tempDir.mkdir()) { 425 return tempDir; 426 } 427 } 428 throw new IllegalStateException("Failed to create directory within " 429 + TEMP_DIR_ATTEMPTS + " attempts (tried " 430 + baseName + "0 to " + baseName + (TEMP_DIR_ATTEMPTS - 1) + ')'); 431 } 432 433 /** 434 * Creates an empty file or updates the last updated timestamp on the 435 * same as the unix command of the same name. 436 * 437 * @param file the file to create or update 438 * @throws IOException if an I/O error occurs 439 */ 440 public static void touch(File file) throws IOException { 441 if (!file.createNewFile() 442 && !file.setLastModified(System.currentTimeMillis())) { 443 throw new IOException("Unable to update modification time of " + file); 444 } 445 } 446 447 /** 448 * Creates any necessary but nonexistent parent directories of the specified 449 * file. Note that if this operation fails it may have succeeded in creating 450 * some (but not all) of the necessary parent directories. 451 * 452 * @throws IOException if an I/O error occurs, or if any necessary but 453 * nonexistent parent directories of the specified file could not be 454 * created. 455 * @since 4.0 456 */ 457 public static void createParentDirs(File file) throws IOException { 458 File parent = file.getCanonicalFile().getParentFile(); 459 if (parent == null) { 460 /* 461 * The given directory is a filesystem root. All zero of its ancestors 462 * exist. This doesn't mean that the root itself exists -- consider x:\ on 463 * a Windows machine without such a drive -- or even that the caller can 464 * create it, but this method makes no such guarantees even for non-root 465 * files. 466 */ 467 return; 468 } 469 parent.mkdirs(); 470 if (!parent.isDirectory()) { 471 throw new IOException("Unable to create parent directories of " + file); 472 } 473 } 474 475 /** 476 * Moves the file from one path to another. This method can rename a file or 477 * move it to a different directory, like the Unix {@code mv} command. 478 * 479 * @param from the source file 480 * @param to the destination file 481 * @throws IOException if an I/O error occurs 482 * @throws IllegalArgumentException if {@code from.equals(to)} 483 */ 484 public static void move(File from, File to) throws IOException { 485 Preconditions.checkNotNull(to); 486 Preconditions.checkArgument(!from.equals(to), 487 "Source %s and destination %s must be different", from, to); 488 489 if (!from.renameTo(to)) { 490 copy(from, to); 491 if (!from.delete()) { 492 if (!to.delete()) { 493 throw new IOException("Unable to delete " + to); 494 } 495 throw new IOException("Unable to delete " + from); 496 } 497 } 498 } 499 500 /** 501 * Reads the first line from a file. The line does not include 502 * line-termination characters, but does include other leading and 503 * trailing whitespace. 504 * 505 * @param file the file to read from 506 * @param charset the character set used when writing the file 507 * @return the first line, or null if the file is empty 508 * @throws IOException if an I/O error occurs 509 */ 510 public static String readFirstLine(File file, Charset charset) 511 throws IOException { 512 return CharStreams.readFirstLine(Files.newReaderSupplier(file, charset)); 513 } 514 515 /** 516 * Reads all of the lines from a file. The lines do not include 517 * line-termination characters, but do include other leading and 518 * trailing whitespace. 519 * 520 * @param file the file to read from 521 * @param charset the character set used when writing the file 522 * @return a mutable {@link List} containing all the lines 523 * @throws IOException if an I/O error occurs 524 */ 525 public static List<String> readLines(File file, Charset charset) 526 throws IOException { 527 return CharStreams.readLines(Files.newReaderSupplier(file, charset)); 528 } 529 530 /** 531 * Streams lines from a {@link File}, stopping when our callback returns 532 * false, or we have read all of the lines. 533 * 534 * @param file the file to read from 535 * @param charset the character set used when writing the file 536 * @param callback the {@link LineProcessor} to use to handle the lines 537 * @return the output of processing the lines 538 * @throws IOException if an I/O error occurs 539 */ 540 public static <T> T readLines(File file, Charset charset, 541 LineProcessor<T> callback) throws IOException { 542 return CharStreams.readLines(Files.newReaderSupplier(file, charset), 543 callback); 544 } 545 546 /** 547 * Process the bytes of a file. 548 * 549 * <p>(If this seems too complicated, maybe you're looking for 550 * {@link #toByteArray}.) 551 * 552 * @param file the file to read 553 * @param processor the object to which the bytes of the file are passed. 554 * @return the result of the byte processor 555 * @throws IOException if an I/O error occurs 556 */ 557 public static <T> T readBytes(File file, ByteProcessor<T> processor) 558 throws IOException { 559 return ByteStreams.readBytes(newInputStreamSupplier(file), processor); 560 } 561 562 /** 563 * Computes and returns the checksum value for a file. 564 * The checksum object is reset when this method returns successfully. 565 * 566 * @param file the file to read 567 * @param checksum the checksum object 568 * @return the result of {@link Checksum#getValue} after updating the 569 * checksum object with all of the bytes in the file 570 * @throws IOException if an I/O error occurs 571 */ 572 public static long getChecksum(File file, Checksum checksum) 573 throws IOException { 574 return ByteStreams.getChecksum(newInputStreamSupplier(file), checksum); 575 } 576 577 /** 578 * Computes and returns the digest value for a file. 579 * The digest object is reset when this method returns successfully. 580 * 581 * @param file the file to read 582 * @param md the digest object 583 * @return the result of {@link MessageDigest#digest()} after updating the 584 * digest object with all of the bytes in this file 585 * @throws IOException if an I/O error occurs 586 */ 587 public static byte[] getDigest(File file, MessageDigest md) 588 throws IOException { 589 return ByteStreams.getDigest(newInputStreamSupplier(file), md); 590 } 591 592 /** 593 * Fully maps a file read-only in to memory as per 594 * {@link FileChannel#map(java.nio.channels.FileChannel.MapMode, long, long)}. 595 * 596 * <p>Files are mapped from offset 0 to its length. 597 * 598 * <p>This only works for files <= {@link Integer#MAX_VALUE} bytes. 599 * 600 * @param file the file to map 601 * @return a read-only buffer reflecting {@code file} 602 * @throws FileNotFoundException if the {@code file} does not exist 603 * @throws IOException if an I/O error occurs 604 * 605 * @see FileChannel#map(MapMode, long, long) 606 * @since 2.0 607 */ 608 public static MappedByteBuffer map(File file) throws IOException { 609 return map(file, MapMode.READ_ONLY); 610 } 611 612 /** 613 * Fully maps a file in to memory as per 614 * {@link FileChannel#map(java.nio.channels.FileChannel.MapMode, long, long)} 615 * using the requested {@link MapMode}. 616 * 617 * <p>Files are mapped from offset 0 to its length. 618 * 619 * <p>This only works for files <= {@link Integer#MAX_VALUE} bytes. 620 * 621 * @param file the file to map 622 * @param mode the mode to use when mapping {@code file} 623 * @return a buffer reflecting {@code file} 624 * @throws FileNotFoundException if the {@code file} does not exist 625 * @throws IOException if an I/O error occurs 626 * 627 * @see FileChannel#map(MapMode, long, long) 628 * @since 2.0 629 */ 630 public static MappedByteBuffer map(File file, MapMode mode) 631 throws IOException { 632 if (!file.exists()) { 633 throw new FileNotFoundException(file.toString()); 634 } 635 return map(file, mode, file.length()); 636 } 637 638 /** 639 * Maps a file in to memory as per 640 * {@link FileChannel#map(java.nio.channels.FileChannel.MapMode, long, long)} 641 * using the requested {@link MapMode}. 642 * 643 * <p>Files are mapped from offset 0 to {@code size}. 644 * 645 * <p>If the mode is {@link MapMode#READ_WRITE} and the file does not exist, 646 * it will be created with the requested {@code size}. Thus this method is 647 * useful for creating memory mapped files which do not yet exist. 648 * 649 * <p>This only works for files <= {@link Integer#MAX_VALUE} bytes. 650 * 651 * @param file the file to map 652 * @param mode the mode to use when mapping {@code file} 653 * @return a buffer reflecting {@code file} 654 * @throws IOException if an I/O error occurs 655 * 656 * @see FileChannel#map(MapMode, long, long) 657 * @since 2.0 658 */ 659 public static MappedByteBuffer map(File file, MapMode mode, long size) 660 throws FileNotFoundException, IOException { 661 RandomAccessFile raf = 662 new RandomAccessFile(file, mode == MapMode.READ_ONLY ? "r" : "rw"); 663 664 boolean threw = true; 665 try { 666 MappedByteBuffer mbb = map(raf, mode, size); 667 threw = false; 668 return mbb; 669 } finally { 670 Closeables.close(raf, threw); 671 } 672 } 673 674 private static MappedByteBuffer map(RandomAccessFile raf, MapMode mode, 675 long size) throws IOException { 676 FileChannel channel = raf.getChannel(); 677 678 boolean threw = true; 679 try { 680 MappedByteBuffer mbb = channel.map(mode, 0, size); 681 threw = false; 682 return mbb; 683 } finally { 684 Closeables.close(channel, threw); 685 } 686 } 687 688 /** 689 * Returns the lexically cleaned form of the path name, <i>usually</i> (but 690 * not always) equivalent to the original. The following heuristics are used: 691 * 692 * <ul> 693 * <li>empty string becomes . 694 * <li>. stays as . 695 * <li>fold out ./ 696 * <li>fold out ../ when possible 697 * <li>collapse multiple slashes 698 * <li>delete trailing slashes (unless the path is just "/") 699 * </ul> 700 * 701 * These heuristics do not always match the behavior of the filesystem. In 702 * particular, consider the path {@code a/../b}, which {@code simplifyPath} 703 * will change to {@code b}. If {@code a} is a symlink to {@code x}, {@code 704 * a/../b} may refer to a sibling of {@code x}, rather than the sibling of 705 * {@code a} referred to by {@code b}. 706 * 707 * @since 11.0 708 */ 709 public static String simplifyPath(String pathname) { 710 if (pathname.length() == 0) { 711 return "."; 712 } 713 714 // split the path apart 715 Iterable<String> components = 716 Splitter.on('/').omitEmptyStrings().split(pathname); 717 List<String> path = new ArrayList<String>(); 718 719 // resolve ., .., and // 720 for (String component : components) { 721 if (component.equals(".")) { 722 continue; 723 } else if (component.equals("..")) { 724 if (path.size() > 0 && !path.get(path.size() - 1).equals("..")) { 725 path.remove(path.size() - 1); 726 } else { 727 path.add(".."); 728 } 729 } else { 730 path.add(component); 731 } 732 } 733 734 // put it back together 735 String result = Joiner.on('/').join(path); 736 if (pathname.charAt(0) == '/') { 737 result = "/" + result; 738 } 739 740 while (result.startsWith("/../")) { 741 result = result.substring(3); 742 } 743 if (result.equals("/..")) { 744 result = "/"; 745 } else if ("".equals(result)) { 746 result = "."; 747 } 748 749 return result; 750 } 751 752 /** 753 * Returns the <a href="http://en.wikipedia.org/wiki/Filename_extension">file 754 * extension</a> for the given file name, or the empty string if the file has 755 * no extension. The result does not include the '{@code .}'. 756 * 757 * @since 11.0 758 */ 759 public static String getFileExtension(String fileName) { 760 checkNotNull(fileName); 761 int dotIndex = fileName.lastIndexOf('.'); 762 return (dotIndex == -1) ? "" : fileName.substring(dotIndex + 1); 763 } 764 }