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 }