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