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 }