User Rating: 0 / 5

Star InactiveStar InactiveStar InactiveStar InactiveStar Inactive
 

Sometimes, I come across the need for creating temporary files in my (Java) programs. For this purpose, I usually use Java's NIO with the Path API instead of the old File API. Here I discuss how a directory for temporary files further be scoped in time with Java's try-with-resources.

1. Introduction

NIO provides the function createTempFile in the class java.nio.file.Files. Since temporary files should go into temporary directories (to group them by purpose), this class also provides the method createTempDirectory. Equipped with all of this goodness, I would be ready to go. However, there are two things that I would like to be maximally convenient:

  1. I want the temporary directory to be managed with Java 7's try-with-resources statement, i.e., to have a scope…
  2. …at whose end it should be automatically deleted (together with whatever files and directories I created inside.

That would allow me to handle temporary files and folders in a way that ensures that nothing useless is left lying around without having to explicitly deleting files and folders when I am done. To implement both of the above features is fairly easy. However, there may be some minor issues with stuff I saw on the web, so I will just outline my approach here.

It should be noted that none of the stuff below has been tested sufficiently well. I cannot guarantee any fitness for use and will not assume any responsibility for anything.

2. Deleting Folders with Everything Inside

I can delete files and folders in Java with the method delete (again of class java.nio.file.Files). However, that will fail if I try to delete a folder which still has a file or another folder inside. Thus, I need to do that recursively. Since I want to mainly delete temporary folders and files, I want my code not to be fail-fast: If something goes wrong (e.g., a file cannot be deleted), it should continue and try to delete as many files and folders as possible and only at the very end throw an exception. This exception should contain as much information about what could not be deleted and why as possible. This makes my situation a bit different from other solutions, such as [1], which is the basis for my solution. I implement it as follows:

Robust (non-fail fast) recursive folder and file deletion under with Java 7's Paths.
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor; 
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;

/** Some helper routines for {@link java.nio.file.Path}-based operations. */
public final class PathUtils {
/**
   * <p>
   * Delete a given path recursively in a robust way.
   * </p>
   * <p>
   * We walk the file tree recursively starting at {@code path} and try to
   * delete any file and directory we encounter. If a deletion fails (with
   * an {@link java.io.IOException}), the method does not fail immediately
   * but instead enqueues the exception in an internal list and continues
   * deleting. At the end, all the caught exceptions together are thrown as
   * {@link java.lang.Throwable#addSuppressed(java.lang.Throwable)
   * suppressed} exceptions inside one {@link java.io.IOException}. Of
   * course, if a single file or folder deletion fails inside a folder, the
   * whole folder may not be deleted. However, as many files as possible
   * and as many sub-folders as possible will be removed. The exception
   * thrown in the end will provide a collection of information about what
   * went wrong. If nothing went wrong, no exception is thrown (obviously).
   * </p>
   * 
   * @param path
   *          the path to delete
   * @throws IOException
   *           if deletion fails
   */
  public static final void delete(final Path path) throws IOException {
    final __DeleteFileTree dft;

    if (path != null) { // if path is null or does not exist, silently exit
      if (Files.exists(path)) { // only now we need to do anything
        dft = new __DeleteFileTree();
        try {
          Files.walkFileTree(path, dft); // recursively delete
        } catch (IOException ioe) {
          dft._storeException(ioe); // remember any additional exception
        }
        dft._throw(path); // throw an IOException if anything failed
      }
    }
  }

  /**
   * A {@link java.nio.file.FileVisitor} implementation which deletes a
   * file tree recursively, by first deleting the files and sub-directories
   * in a directory, then the directory itself.
   */
  private static final class __DeleteFileTree extends
      SimpleFileVisitor< Path > {

    /** the exceptions */
    private ArrayList< Throwable > m_exceptions;

    /** the file tree deleter's constructor */
    __DeleteFileTree() {
      super();
    }

    /**
     * store an exception (synchronized just in case)
     * 
     * @param t
     *          the exception to store
     */
    synchronized final void _storeException(final Throwable t) {
      if (t != null) {
        if (this.m_exceptions == null) {
          this.m_exceptions = new ArrayList<>();
        }
        this.m_exceptions.add(t);
      }
    }

    /**
     * Throw an {@link java.io.IOException}, if necessary: If we
     * {@link #_storeException(java.lang.Throwable) collected} any
     * exceptions during the file and folder visiting process, we now
     * create and throw an {@link java.io.IOException} which states that
     * errors took place during the deletion of the (entry-point)
     * {@code path} and which has all the collected exceptions as
     * {@link java.lang.Throwable#addSuppressed(java.lang.Throwable)
     * suppressed exceptions}.
     *
     * @param path
     *          the root path
     * @throws IOException
     *           if necessary
     */
    final void _throw(final Path path) throws IOException {
      IOException ioe;
      if ((this.m_exceptions != null) && (!(this.m_exceptions.isEmpty()))) {
        ioe = new IOException("Error when deleting '" + path + '\''); //$NON-NLS-1$
        for (Throwable t : this.m_exceptions) {
          ioe.addSuppressed(t);
        }
        throw ioe;
      }
    }

    /** {@iheritDoc} */
    @Override
    public final FileVisitResult visitFile(Path file,
        BasicFileAttributes attrs) {
      try {
        Files.delete(file);
      } catch (Throwable t) {
        this._storeException(t);
      }
      return FileVisitResult.CONTINUE;
    }

    /** {@iheritDoc} */
    @Override
    public final FileVisitResult visitFileFailed(Path file, IOException exc) {
      if (exc != null) {
        this._storeException(exc);
      }
      try {
        Files.delete(file);
      } catch (Throwable t) {
        this._storeException(t);
      }
      return FileVisitResult.CONTINUE;
    }

    /** {@iheritDoc} */
    @Override
    public final FileVisitResult postVisitDirectory(Path dir,
        IOException exc) throws IOException {
      if (exc != null) {
        this._storeException(exc);
      }
      try {
        Files.delete(dir);
      } catch (Throwable t) {
        this._storeException(t);
      }
      return FileVisitResult.CONTINUE;
    }
  }
}

Besides not being fail-fast, this methods differs from my inspirational source [1] in another way: It does not employ any operating-system specific commands (such as rm or rd) but only relies on the Java API. The reasons for this are as follows:

  1. The most important one may be that the Path API is not "platform-bound". It could be implemented for zip-file systems as well or for memory file systems. If you would have such a path and throw it into a platform-native command such as rd, all hell could break loose (because that path may map to anything, I think).
  2. From an OS command, you may not get detailed error information.
  3. Calling an OS command or running a process for each deletion may actually be slower (or faster, who knows, but I like deterministic behavior).
  4. Apropos, deterministic: using OS-dependent commands may introduce slight differences in behavior on different OSes.
  5. Also, you may only be able to implement OS-specific deletion for a small subset of OSes, such as Linux/Unix and Windows anyway. Plus I am not sure how reliable the detection of the OS is from within Java.
  6. And you will actually somewhat depend on the versions of these OSes. Maybe the parameters of the commands change in an ever-so-subtle in the future or already differ amongst the many versions of Windows, for example – at least I wouldn't want to check or maintain that.
  7. Finally, I think a Java program should stay within the boundaries of the virtual machine for as long as possible. I would only call outside processes if absolutely necessary. This is just a general design idea: Reduce outside dependencies, rely on the security settings, configuration, and implementation of the JVM.

Of course, there are also some good reasons for using OS-specific commands. For instance, they can probably better deal with the underlying file systems, better understand stuff such as hard and soft links (although these are maybe not so likely to be in a temporary directory), and they actually may be faster. All in all, it depends on the likings of the programmer which way to go.

3. Temporary Directory Managed with try-with-resources

Java 7's try-with-resources statement allows us to put an instance of AutoCloseable into a construct similar to a try…finally which ensures that its close method is called under all circumstances (unless the JVM is terminated ungracefully before). For I/O related stuff, we would implement its sub-interface Closeable.

We can use this mechanism and the above code to implement a temporary folder which, along with its contents, is deleted at the end of the try-with-resources scope as follows:

A temporary folder that can be managed by Java 7's try-with-resources statement.
import java.io.Closeable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

/**
 * A temporary directory which can be wrapped into a
 * {@code try-with-resources} statement. The directory is created in the
 * constructor and deleted once the {@link #close()} method is called.
 * Within the {@code try-with-resources} statement, you can get the path to
 * the temporary directory via {@link #getPath()}. You can then create as
 * many files and sub-directories inside the temporary directory as you
 * wish, they will all automatically be deleted at the end.
 */
public class TempDir implements Closeable {

  /** the directory */
  private final Path m_dir;

  /**
   * create the temporary directory
   * 
   * @throws IOException
   *           if io fails
   */
  public TempDir() throws IOException {
    super();
    this.m_dir = Files.createTempDirectory(null);
    if (this.m_dir == null) {
      throw new IOException("Failed to create temporary directory."); //$NON-NLS-1$
    }
  }

  /**
   * Get the path of the temporary directory. 
   * @return the path to the directory
   */
  public final Path getPath() {
    return this.m_dir;
  }

  /** {@inheritDoc} */
  @Override
  public final void close() throws IOException {
    PathUtils.delete(this.m_dir);
  }
}

4. How to Use

This temporary folder can now be used as follows:

An example for using the scoped temporary folder.
import java.nio.file.Files;
import java.nio.file.Path;

/** Here we test the temporary folder API. */
public final class TempDirExample {

  /**
   * The main routine 
   * @param args
   *          ignored
   */
  public static final void main(final String[] args) {
    Path tempPath, file;

    try {
      tempPath = null;
      try (final TempDir temp = new TempDir()) {
        tempPath = temp.getPath();

        System.out.println("Inside try-with-resources scope"); //$NON-NLS-1$

        System.out.println("Temp path is: " + tempPath); //$NON-NLS-1$
        System.out.println(tempPath + " exists: " + //$NON-NLS-1$
            Files.exists(tempPath));

        file = tempPath.resolve("test.txt");//$NON-NLS-1$
        Files.createFile(file);

        System.out.println(file + " exists: " + Files.exists(file));//$NON-NLS-1$        
      }

      System.out.println("After try-with-resources scope"); //$NON-NLS-1$
      System.out.println(tempPath + " exists: " + //$NON-NLS-1$
          Files.exists(tempPath));
      System.out.println(file + " exists: " + Files.exists(file));//$NON-NLS-1$       
    } catch (Throwable ttt) {
      ttt.printStackTrace(); // this should not happen
    }
  }
}

Running the above program (under Linux) should result in something like:

Example for Program Output.
Inside try-with-resources scope
Temp path is: /tmp/7939536986162702754
/tmp/7939536986162702754 exists: true
/tmp/7939536986162702754/test.txt exists: true
After try-with-resources scope
/tmp/7939536986162702754 exists: false
/tmp/7939536986162702754/test.txt exists: false

This means that a temporary folder was created. We then created a file inside that folder. Once the closing curly brace of the try-with-resources was reached, both have automatically been deleted.

P.S. A reason why I used NIO with the Path API is that it is not bound to real files on disk. One day, I may be able to use a file system in memory for some of the temporary files. In that case, my delete method would still work all the same.