Friday, November 25, 2011

File Management in Java with Guava's Files Class

Both Groovy and Java SE 7 provide improvements for file management in Java as I discussed in posts here, here, here, and here. However, when a particular Java application is not able to yet use Java SE 7 or Groovy for its file management, an improved file management experience can still be obtained by using Guava's Files class.

File handling has always seemed a little more difficult in Java than in many other programming languages. Java SE 7 certainly improves Java's file handling capabilities dramatically with NIO.2, but not every project can make use of Java SE 7. For those projects not able to use Java SE 7, Guava's Files class is a nice intermediate solution for easier file handling. It is important to note here that Java SE 7 introduces a Files class of its own, so any use of Guava's Files class in Java SE 7 must be fully scoped if the Java version of Files is used in the same code. My examples have been written and built in Java SE 7, but I avoid using Java SE 7's Files class and so don't need to fully scope Guava's same-named class.

File Creation

Guava's Files class includes a couple overloaded write methods for easily writing content to a file. The next code sample demonstrates using Files.write(byte[],File).

Demonstrating Files.write(byte[],File)
   /**
    * Demonstrate writing bytes to a specified file.
    * 
    * @param fileName Name of file to be written to.
    * @param contents Contents to be written to file.
    */
   public void demoFileWrite(final String fileName, final String contents)
   {
      checkNotNull(fileName, "Provided file name for writing must NOT be null.");
      checkNotNull(contents, "Unable to write null contents.");
      final File newFile = new File(fileName);
      try
      {
         Files.write(contents.getBytes(), newFile);
      }
      catch (IOException fileIoEx)
      {
         err.println(  "ERROR trying to write to file '" + fileName + "' - "
                     + fileIoEx.toString());
      }
   }

There are a couple observations that can be from this first code sample that will apply to all other code samples in this post. First, I make use of the statically imported Guava Preconditions class to provide an easy check ensuring that the provided parameters are not null. The second common feature of this code is that it explicitly catches and "handles" the checked exception IOException. All other Files methods demonstrated in this post similarly throw this same checked exception.

File Copying

The next example demonstrates how easy it is to copy files using Guava's Files.copy(File,File) method (one of several overloaded methods with name 'copy').

Demonstrating Files.copy(File,File)
   /**
    * Demonstrate simple file copying in Guava. This demonstrates one of the
    * numerous overloaded copy methods provided by Guava's Files class. The
    * version demonstrated here copies one provided File instance to another
    * provided File instance.
    * 
    * @param sourceFileName Name of file that is to be copied.
    * @param targetFileName Name of file that is result of file copying.
    */
   public void demoSimpleFileCopy(
      final String sourceFileName, final String targetFileName)
   {
      checkNotNull(sourceFileName, "Copy source file name must NOT be null.");
      checkNotNull(targetFileName, "Copy target file name must NOT be null.");
      final File sourceFile = new File(sourceFileName);
      final File targetFile = new File(targetFileName);
      try
      {
         Files.copy(sourceFile, targetFile);
      }
      catch (IOException fileIoEx)
      {
         err.println(
              "ERROR trying to copy file '" + sourceFileName
            + "' to file '" + targetFileName + "' - " + fileIoEx.toString());
      }
   }
File Moving

Moving files with Guava is as easy as copying. The Files.move(File,File) method is demonstrated in the next code snippet.

Demonstrating Files.move(File,File)
  /**
    * Demonstrate moving a file with Guava's Files.move(File,File).
    * 
    * @param sourceFileName Path/name of File to be moved.
    * @param targetFileName Path/name of Destination of file.
    */
   public void demoMove(final String sourceFileName, final String targetFileName)
   {
      checkNotNull(sourceFileName, "Move source file name must NOT be null.");
      checkNotNull(targetFileName, "Move destination name must NOT be null.");
      final File sourceFile = new File(sourceFileName);
      final File targetFile = new File(targetFileName);
      try
      {
         Files.move(sourceFile, targetFile);
      }
      catch (IOException fileIoEx)
      {
         err.println(
              "ERROR trying to move file '" + sourceFileName
            + "' to '" + targetFileName + "' - " + fileIoEx.toString());
      }
   }
Comparing Files

Determining if two files are the same is straightforward with the Gauva Files.equal(File,File) method.

Demonstrating Files.equal(File,File)
   /**
    * Demonstrate using Guava's Files.equal(File,File) to compare contents of
    * two files.
    * 
    * @param fileName1 Name of first file to be compared.
    * @param fileName2 Name of second file to be compared.
    */
   public void demoEqual(final String fileName1, final String fileName2)
   {
      checkNotNull(fileName1, "First file name for comparison must NOT be null.");
      checkNotNull(fileName2, "Second file name for comparison must NOT be null.");
      final File file1 = new File(fileName1);
      final File file2 = new File(fileName2);
      try
      {
         out.println(
             "File '" + fileName1 + "' "
           + (Files.equal(file1, file2) ? "IS" : "is NOT")
           + " the same as file '" + fileName2 + "'.");
      }
      catch (IOException fileIoEx)
      {
         err.println(
              "ERROR trying to compare two files '"
            + fileName1 + "' and '" + fileName2 + "' - " + fileIoEx.toString());
      }
   }
Touching Files

Touching a file to create a new empty file or to update the timestamp on an existing file can be easily accomplished with Guava's Files.touch(File) as shown in the next code sample.

Demonstrating Files.touch(File)
   /**
    * Demonstrate Guava's Files.touch(File) method.
    * 
    * @param fileNameToBeTouched Name of file to be 'touch'-ed.
    */
   public void demoTouch(final String fileNameToBeTouched)
   {
      checkNotNull(fileNameToBeTouched, "Unable to 'touch' a null filename.");
      final File fileToTouch = new File(fileNameToBeTouched);
      try
      {
         Files.touch(fileToTouch);
      }
      catch (IOException fileIoEx)
      {
         err.println(
              "ERROR trying to touch file '" + fileNameToBeTouched
            + "' - " + fileIoEx.toString());
      }
   }
Retrieving File Contents

With a simplicity reminiscent of Groovy's GDK extension File.getText(), Guava's Files.toString(File,Charset) makes it easy to retrieve text contents of a file.

Demonstrating Files.toString(File,Charset)
   /**
    * Demonstrate retrieving text contents of a specified file with Guava's 
    * Files.toString(File) method.
    * 
    * @param nameOfFileToGetTextFrom Name of file from which text is to be
    *    retrieved.
    */
   public void demoToString(final String nameOfFileToGetTextFrom)
   {
      checkNotNull(nameOfFileToGetTextFrom, "Unable to retrieve text from null.");
      final File sourceFile = new File(nameOfFileToGetTextFrom);
      try
      {
         final String fileContents = Files.toString(sourceFile, Charset.defaultCharset());
         out.println(
              "Contents of File '" + nameOfFileToGetTextFrom
            + "' are: " + fileContents);
      }
      catch (IOException fileIoEx)
      {
         err.println(
              "ERROR trying to get text contents of file '"
            + nameOfFileToGetTextFrom + "' - " + fileIoEx.toString());
      }
   }
Temporary Directory Creation

Guava makes it easy to generate a temporary directory with Files.createTempDir().

Demonstrating Files.createTempDir()
   /**
    * Demonstrate Guava's Files.createTempDir() method for creating a temporary
    * directory.
    */
   public void demoTemporaryDirectoryCreation()
   {
      final File newTempDir = Files.createTempDir();
      try
      {
         out.println(
            "New temporary directory is '" + newTempDir.getCanonicalPath() + "'.");
      }
      catch (IOException ioEx)
      {
         err.println("ERROR: Unable to create temporary directory - " + ioEx.toString());
      }
   }

I don't provide a code sample here, but it's worth noting that Guava provides a convenience method for creating a new directory will all necessary new parent directories with its Files.createParentDirs(File) method.

Retrieving Contents of File as Lines

There are times when it is most convenient to get the contents of a file as a series of lines so that each line can be processed. This is commonly done in Groovy with overloaded versions of readLines() and eachLine(). Guava provides similar functionality to Groovy's File.readLines() with its Files.readLines(File, Charset) method. This is demonstrated in the following code sample.

Demonstrating Files.readLines(File,Charset)
   /**
    * Demonstrate extracting lines from file.
    * 
    * @param fileName Name of file from which lines are desired.
    */
   public void demoRetrievingLinesFromFile(final String fileName)
   {
      final File file = new File(fileName);
      try
      {
         final List<String> lines = Files.readLines(file, Charset.defaultCharset());
         for (final String line : lines)
         {
            out.println(">> " + line);
         }
      }
      catch (IOException ioEx)
      {
         err.println(
              "ERROR trying to retrieve lines from file '"
            + fileName + "' - " + ioEx.toString());
      }
   }

The other overloaded version of readLines is interesting because it allows a LineProcessor callback to be specified to terminate returning of lines earlier than the end of the file.

Reading File's First Line

I have run into numerous situations in which it has been useful to read only the first line of file. This first line might tell my code what type of script is being run, provide XML prolog information, or other interesting overview data of a file's contents. Guava makes it easy to retrieve just the first line with the Files.readFirstLine(File,Charset) method. This is demonstrated in the next code listing.

Demonstrating Files.readFirstLine(File,Charset)
   /**
    * Demonstrate extracting first line of file.
    * 
    * @param fileName File from which first line is to be extracted.
    */
   public void demoRetrievingFirstLineFromFile(final String fileName)
   {
      final File file = new File(fileName);
      try
      {
         final String line = Files.readFirstLine(file, Charset.defaultCharset());
         out.println("First line of '" + fileName + "' is '" + line + "'.");
      }
      catch (IOException fileIoEx)
      {
         err.println(
              "ERROR trying to retrieve first line of file '"
            + fileName + "' - " + fileIoEx.toString());
      }
   }
There's Much More

Although I have discussed and demonstrated several useful Files methods in this post, the class has many more to offer. Some of these include the ability to append to an existing file with Files.append(CharSequence,File,Charset), getting a file's checksum with Files.getChecksum(File,Checksum), getting a file's digest with Files.getDigest(File,MessageDigest), accessing a BufferedReader with Files.newReader(File,Charset), accessing a BufferedWriter with Files.newWriter(File,Charset), and accessing a MappedByteBuffer mapped to an underlying file via overloaded Files.map methods.

Conclusion

File handling in Java is much easier and more convenient with Guava's Files class. Guava brings file-handling convenience to Java applications that cannot make use of Groovy's or Java SE 7's file handling conveniences.

2 comments:

Unknown said...

Helpful post. Thanks. Couple of small clean ups: In your example, Files.readLines, the source won't compile because string is lower-cased in the for loop. Also, there are some formatting garbage bits at the bottom of the listing.

@DustinMarx said...

Unknown,

Thanks for pointing those issues out. They are both due to the same issue: I forgot to convert the less-than sign in my code to its entity reference equivalent before posting. This caused the browser to interpret the generic using String as an HTML tag (presenting it in lower case) and the actual generic was treated as an HTML tag at the end of the code listing.

Thanks for letting me know; it's fixed now.

Dustin