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