Java Cookbook 10.X Save User Data to Disk

Problem

You need to save user data to disk. This may be in response to "File->Save" in a GUI application, saving the file in a text editor, or saving configuration data in a non-gui application. You have heard (correctly) that a well-behaved application should never lose data.

Solution

Use this five-step plan, with appropriate variations:

Discussion

As developers we have to deal with the fact that saving a file to disk is full of risk. There are many things that can go wrong in saving data, yet it is one of the most critical parts of most applications. If you lose data that a person has spent hours inputting, or even lost a setting that a user feels strongly about, they will despise your whole application. The disk might fill up while we're writing it, or be full before we start. This is a user's error, but we have to face it. If you are transforming data, say getting it from a JDBC ResultSet (Chapter 20) or writing objects using an XMLEncoder (xxxref), an exception could be thrown. If you're not careful, these exceptions can cause the user's data to be lost.

So here's a more detailed discussion of the little five-step dance we should go through.

This may seem like overkill, but "It's not overkill, it prevents career kill". I've done pretty much this in numerous apps with various save file formats. This plan is only really safe way around all the problems that can occur. For example, the final step has to be a rename not a copy, regardless of size considerations, to avoid the "disk fills up" problem. So, to be correct, you have to ensure that the temp file gets created on the same disk partition (drive letter or mount point) as the user's file,

Code

/*
 * Copyright Notice:
 * This code is copyright by Ian Darwin but is BSD licensed and
 * can thus be used for anything by anybody.
 * If you get rich off it, send me all your money. :-)
 */

package com.darwinsys.io;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;

/**
 * Save a user data file, as safely as we can.
 * The basic algorithm is:
 
  1. We create a temporary file, in the same directory as the input file so we can safely rename it. Set it to with deleteOnExit(true);
  2. Our client writes the user data to this file. Data format or translation errors, if any, will be thrown during this process, leaving the user's original file intact. Client closes file.
  3. We delete the previous backup file, if one exists;
  4. We rename the user's previous file to filename.bak;
  5. We rename the temporary file to the save file.
* This algorithm all but guarantees not to fail for reasons of * disk full, permission denied, etc. Alternate algorithms could * be employed that would preserve the original file ownership and * permissions (e.g., on POSIX filesystems) but they can not then * guarantee not to fail due to disk full conditions. *

* Step 1 is implemented in the constructor. * Step 2 you do, by calling getWriter or getOutputStream (not both). * Step 3, 4 and 5 are done in finish(). *

* Normal usage is thus: *

 * try {
 * 	FileSaver saver = new FileSaver(file);
 * 	final Writer writer = saver.getWriter();
 * 	PrintWriter out = new PrintWriter(writer);
 * 	myWriteOutputFile(out);
 * 	out.close();
 * 	saver.finish();
 * 	System.out.println("Saved OK");
 * } catch (IOException e) {
 * 	System.out.println("Save FAILED");
 * }
 * 
*

* Objects of this class may be re-used sequentially (for the * same file) but are not thread-safe and should not be shared * among different threads. * @author Extracted and updated by Ian Darwin from an older * application, prompted by discussion started by Brendon McLean * on a private mailing list. */ public class FileSaver { private enum State { /** The state before and after use */ AVAILABLE, /** The state while in use */ INUSE } private State state; private final File inputFile; private final File tmpFile; private final File backupFile; public FileSaver(File input) throws IOException { // Step 1: Create temp file in right place this.inputFile = input; tmpFile = new File(inputFile.getAbsolutePath() + ".tmp"); tmpFile.createNewFile(); tmpFile.deleteOnExit(); backupFile = new File(inputFile.getAbsolutePath() + ".bak"); state = State.AVAILABLE; } /** * Return a reference to the contained File object, to * promote re-use (File objects are immutable so this * is at least moderately safe). Typical use would be: *

	 * if (fileSaver == null ||
	 *   !(fileSaver.getFile().equals(file))) {
	 *		fileSaver = new FileSaver(file);
	 * }
	 * 
*/ public File getFile() { return inputFile; } /** Return an output file that the client should use to * write the client's data to. * @return An OutputStream, which should be wrapped in a * buffered OutputStream to ensure reasonable performance. * @throws IOException if the temporary file cannot be written */ public OutputStream getOutputStream() throws IOException { if (state != State.AVAILABLE) { throw new IllegalStateException("FileSaver not opened"); } OutputStream out = new FileOutputStream(tmpFile); state = State.INUSE; return out; } /** Return an output file that the client should use to * write the client's data to. * @return A Writer, which should be wrapped in a * buffered Writer to ensure reasonable performance. * @throws IOException if the temporary file cannot be written */ public Writer getWriter() throws IOException { if (state != State.AVAILABLE) { throw new IllegalStateException("FileSaver not opened"); } Writer out = new FileWriter(tmpFile); state = State.INUSE; return out; } /** Close the output file and rename the temp file to the original name. * @throws IOException If anything goes wrong */ public void finish() throws IOException { if (state != State.INUSE) { throw new IllegalStateException("FileSaver not in use"); } // Delete the previous backup file if it exists; backupFile.delete(); // Rename the user's previous file to itsName.bak, // UNLESS this is a new file ; if (inputFile.exists() && !inputFile.renameTo(backupFile)) { throw new IOException("Could not rename file to backup file"); } // Rename the temporary file to the save file. if (!tmpFile.renameTo(inputFile)) { throw new IOException("Could not rename temp file to save file"); } state = State.AVAILABLE; } }

See Also

The Preferences API (Recipe 7.7) allows you to save small amounts of preference data, but is not convenient for saving program state. The Object Serialization API (Recipe 10.18) and the XMLEncoder/XMLDecoder (Recipe 21.1) will serialize objects to/from an external representation. The XML forms have the benefit that they store text files; somewhat larger, but more portable.

Acknowledgements

The code is my own, based on my own experience in various applications. I was prompted to package it up this way, and write it up, by a posting made by Brendon McLean to the "Java Application Framework" (JSR-XXX) mailing list in April, 2007.