• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2007 The Guava Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.common.io;
18 
19 import static com.google.common.base.Preconditions.checkNotNull;
20 
21 import com.google.common.annotations.Beta;
22 import com.google.common.base.Joiner;
23 import com.google.common.base.Preconditions;
24 import com.google.common.base.Splitter;
25 
26 import java.io.BufferedReader;
27 import java.io.BufferedWriter;
28 import java.io.Closeable;
29 import java.io.File;
30 import java.io.FileInputStream;
31 import java.io.FileNotFoundException;
32 import java.io.FileOutputStream;
33 import java.io.IOException;
34 import java.io.InputStream;
35 import java.io.InputStreamReader;
36 import java.io.OutputStream;
37 import java.io.OutputStreamWriter;
38 import java.io.RandomAccessFile;
39 import java.nio.MappedByteBuffer;
40 import java.nio.channels.FileChannel;
41 import java.nio.channels.FileChannel.MapMode;
42 import java.nio.charset.Charset;
43 import java.security.MessageDigest;
44 import java.util.ArrayList;
45 import java.util.List;
46 import java.util.zip.Checksum;
47 
48 /**
49  * Provides utility methods for working with files.
50  *
51  * <p>All method parameters must be non-null unless documented otherwise.
52  *
53  * @author Chris Nokleberg
54  * @since 1.0
55  */
56 @Beta
57 public final class Files {
58 
59   /** Maximum loop count when creating temp directories. */
60   private static final int TEMP_DIR_ATTEMPTS = 10000;
61 
Files()62   private Files() {}
63 
64   /**
65    * Returns a buffered reader that reads from a file using the given
66    * character set.
67    *
68    * @param file the file to read from
69    * @param charset the character set used when writing the file
70    * @return the buffered reader
71    */
newReader(File file, Charset charset)72   public static BufferedReader newReader(File file, Charset charset)
73       throws FileNotFoundException {
74     return new BufferedReader(
75         new InputStreamReader(new FileInputStream(file), charset));
76   }
77 
78   /**
79    * Returns a buffered writer that writes to a file using the given
80    * character set.
81    *
82    * @param file the file to write to
83    * @param charset the character set used when writing the file
84    * @return the buffered writer
85    */
newWriter(File file, Charset charset)86   public static BufferedWriter newWriter(File file, Charset charset)
87       throws FileNotFoundException {
88     return new BufferedWriter(
89         new OutputStreamWriter(new FileOutputStream(file), charset));
90   }
91 
92   /**
93    * Returns a factory that will supply instances of {@link FileInputStream}
94    * that read from a file.
95    *
96    * @param file the file to read from
97    * @return the factory
98    */
newInputStreamSupplier( final File file)99   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 }
765