Google Guava: input/output operations

Working with files in Java, and more precisely, reading file streams, was always a little bit complicated. It's the reason why each new version of Java contains some improvements for that. And Google Guava, once again, appears as an alternative.

This time we'll cover the part of the library about I/O operations. At the begin we'll see which classes are provided by Google Guava to handle input and output operations. After that, in the second part, we'll show how to work with them through some JUnit test cases.

Input, output and Google Guava

Some classes compose com.google.common.io package. The first part described here concerns the classes responsible for writing and reading from and to streams. The readers implements InputSupplier<T> interface. This typed interface defines a single method, getInput(), which returns an object of suppliers type. The writers implement OutputSupplier<T> interface which contains a single method, getOutput(). Even that both interfaces are marked as deprecated, they are still in use by no-deprecated classes used to read and write bytes and chars.

The classes operating on bytes are ByteSink and ByteSource. They're both abstract, so we can deduct that they are only tool classes. Another sink and source classes, CharSink and CharSource, have for vocation to deal with characters. If you look in source code of read (read()) and write (write(byte[] bytes)) methods, you'll see that they're both working with an object represented by Closer instance.

Closer is next key concept of Google Guava's IO package. This final public class implementing Closeable interface contains some method which handling open/close states:
- register(@Nullable C closeable): registers an instance of object to be closed when given Closer is closed.
- close(): this method closes all Closeable instances added to given Closer object.

Thanks to Closer, we can compact the code needed to handle opening and closing streams. When the streams are processed, we can be notified every time when a new line or a chunk of bytes is read. It's thanks to two defined listeners, LineProcessor<T> and ByteProcessor<T>. As you can see through theirs names, they can be applied to char or bytes readers. They both define process* methods (processBytes for ByteProcessor and processLine for LineProcessor). These methods return a boolean. If this result is false, the reading is stopped.

One more interesting tool class is Files. It permits to convert given File instances to ByteSink or ByteSource. These instances are represented by private classes, FileByteSink and FileByteSource. It contains also some tool methods to normalize paths, get file information (extension, name without extension) or even to imitate Linux touch file command.

Example of I/O operations in Google Guava

This is a simple test cases which illustrate how to implement Google Guava I/O mechanism:

public class InputOutputTest {

  @Test
  public void files() {
     /**
      * In this test we focus on file utilities. The first one is path normalization.
      */
    String normalizedPath = Files.simplifyPath("../home/./bartosz/tmp/./work");
    String expectedPath = "../home/bartosz/tmp/work";
    assertTrue("Expected the path ("+expectedPath+") is not the same as normalized ("+normalizedPath+")", 
      expectedPath.equals(normalizedPath));
    
     /**
      * Here we can see how to analyze file names with Google Guava.
      */
    String file = "my.text.file.txt";
    String ext = Files.getFileExtension(file);
    String name = Files.getNameWithoutExtension(file);
    String expectedExt = "txt";
    String expectedName = "my.text.file";
    assertTrue("Expected extension ("+expectedExt+") is not the same as received ("+ext+")", 
      expectedExt.equals(ext));
    assertTrue("Expected name ("+expectedName+") is not the same as received ("+name+")", 
      expectedName.equals(name));

     /**
      * This case shows how to imitate Linux touch event with Google Guava.
      */
    File testFile = new File("/home/bartosz/tmp/testFile.txt");
    testFile.delete(); // by security, remove file before writting it
    List<String> expectedLines = new ArrayList<String>();
    expectedLines.add("FIRST line");
    expectedLines.add("SECOND line");
    expectedLines.add("THIRD line");
    expectedLines.add("French accentuated letter: étrangère");

    writeToFile(testFile, expectedLines);

    try {
      long initialTouch = testFile.lastModified();
      Thread.sleep(1000);
      Files.touch(testFile);
      long afterTouch = testFile.lastModified();
      assertTrue("Touch should be correctly done on the file but it wasn't", afterTouch > initialTouch);
    } catch (Exception e) {
      testFile.delete();
      fail("Touch file failure: "+e.getMessage());
    }
    
     /**
      * And this one reads file content to a List of String instances. File content is:
      * <pre>
      * FIRST line
      * SECOND line
      * THIRD line
      * French accentuated letter: étrangère
      * </pre>
      */
    try {
      List<String> fileLines = Files.readLines(testFile, Charset.forName("UTF-8"));
      assertTrue("Reader should read "+expectedLines.size()+" lines but it only read "+fileLines.size(), 
        fileLines.size() == expectedLines.size());
      for (int i = 0; i < fileLines.size(); i++) {
        assertTrue("Expected line ("+expectedLines.get(i)+") is not the same as read line ("+fileLines.get(i)+")",
          fileLines.get(i).equals(expectedLines.get(i)));
      }
    } catch (Exception e) {
      fail("Failure on reading "+testFile+": "+e.getMessage());
    } finally {
      testFile.delete();
    }
  }

  @Test
  public void streams() {
     /**
      * First tested thing will be stream closing in Closer's close() method. To test that, 
      * we'll register two streams into Closer and check after if ready() methods can be invoked. 
      * Normally, this invocation should throw IOException: 
      * From Javadoc of Reader close() method:
      * <pre>
      * Closes the stream and releases any system resources associated with it. Once the stream has been closed, further read(), 
      * ready(), mark(), reset(), or skip() invocations will throw an IOException. Closing a previously closed stream has no effect.
      *  </pre>
      */
    List<String> expectedLines = new ArrayList<String>();
    expectedLines.add("FIRST line");
    expectedLines.add("SECOND line");
    expectedLines.add("THIRD line");
    expectedLines.add("French accentuated letter: étrangère");
    File testFile = new File("/home/bartosz/tmp/testFile.txt");
    writeToFile(testFile, expectedLines);

    Closer closer = Closer.create();
    Reader reader = null;
    try {
    CharSource fileSource = Files.asCharSource(testFile, Charset.forName("UTF-8"));
    reader = closer.register(fileSource.openStream());
    } catch (Exception e) {
      fail("Reading from CharSource failed because of: "+e.getMessage());
    } finally {
      try {
        closer.close();
      } catch (IOException e) {
        fail("Closer close() failed because of: "+e.getMessage());
      }
    }
    // Normally Reader should be closed by Closer. So the invocation of its ready() method, according to the Javadoc, will thrown IOException
    try {
      reader.ready();
      fail("Reader shouldn't be able to call ready() after closing (so the close wasn't done correctly)");
    } catch (Exception e) {
      assertTrue("Exception should be IOException but is "+e.getClass(), 
        e.getClass() == IOException.class);
    }
     /**
      * Here we'll create new file, fill up with some content. After that we'll try to read it with LineProcessor and interrupt the reading after 2 lines.
      */
    List<String> linesToWrite = new ArrayList<String>();
    linesToWrite.add("1st line");
    linesToWrite.add("2nd line");
    linesToWrite.add("3rd line");
    File anotherTextFile = new File("/home/bartosz/tmp/anotherTestFile.txt");
    writeToFile(anotherTextFile, linesToWrite);

    CharSource fileSource = Files.asCharSource(anotherTextFile, Charset.forName("UTF-8"));
    String lines = fileSource.readLines(new LineProcessor<String>() {
      private StringBuilder result = new StringBuilder();
      private int linesProcessed = 1;
      @Override
      public String getResult() {
        return result.toString();
      }

      @Override
      public boolean processLine(String line) throws IOException {
        result.append(line).append("\n");
        linesProcessed++;
        return linesProcessed < 3;
      }
    });
    String[] parts = lines.split("\n");
    assertTrue("Read text should be 2-lines length but is "+parts.length, 
      parts.length == 2);
    assertTrue("The first written line ("+linesToWrite.get(0)+") should be the same as the first read line ("+parts[0]+")", 
      linesToWrite.get(0).equals(parts[0]));
    assertTrue("The second written line ("+linesToWrite.get(1)+") should be the same as the second read line ("+parts[1]+")", 
      linesToWrite.get(1).equals(parts[1]));
    assertFalse("Read text shouldn't contain the third written line (break after 2 lines read, not expected line is :"+linesToWrite.get(2)+")", 
      lines.contains(linesToWrite.get(2)));
    
    // cleanup
    anotherTextFile.delete();
  }

  private void writeToFile(File file, List<String> linesToWrite) {
    try {
      CharSink writer = Files.asCharSink(file, Charset.forName("UTF-8"), FileWriteMode.APPEND);
      writer.writeLines(linesToWrite);
    } catch (Exception e) {
      file.delete();
      fail("Writting failure to the file "+file+": "+e.getMessage());
    }
  }
}

Through this article we can see how to work with input and output operations with the help of Google Guava. We saw that the stream closing can be made thanks to Closer register() and close() methods. After that we discovered that this library provides some interesting stuff to handle file names, as path normalization or extension extraction. At the end we saw the shortcuts to write and read a file through sink and source (as well as bytes as char).


If you liked it, you should read:

📚 Newsletter Get new posts, recommended reading and other exclusive information every week. SPAM free - no 3rd party ads, only the information about waitingforcode!