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