Java Preferences using a file as the backing store

Submitted by davidc on Thu, 18/06/2009 - 18:19

java.util.prefs is great. Since it arrived, I've not had to worry about storing configuration in property files, or where those files should go. It abstracts away any platform differences and stores the preferences in a suitable place for the platform, e.g. in the registry under Windows.

I recently received a request to allow an application to have its configuration stored in a file instead of the registry. The user wanted it to run on any computer from a flash drive, with the configuration moving too. You can easily change the Preferences implementation using the java.util.prefs.PreferencesFactory system property, but the Sun JVM only ships with the platform-specific Preferences implementation, i.e. WindowsPreferences under Windows. I therefore needed to create my own Preferences implementation, which is detailed in this article.

File storage

I elected to store all the preferences in a single file, rather than a directory structure as used by the Mac and Linux Preferences implementations. The file is a standard Java Properties file, with each key being the concatenation of the package name and the preference name.

The file used is determined by the system property net.infotrek.util.prefs.FilePreferencesFactory.file if it exists, otherwise it defaults to .fileprefs in user.home. I elected to store both system and user preferences in the same file.

Note that you must set the factory in the system property java.util.prefs.PreferencesFactory before any use of the Preferences API; either through a command-line -D, or early on in your startup code.

Implementation

AbstractPreferences is a useful starting point, performing a lot of the grunt work of the Preferences API for you, leaving just nine methods to implement.

One thing to note is that I have made a call to sync() on instantiation and flush() on update. This is because updates were not being flushed automatically, despite the claim "Normal termination of the Java Virtual Machine will not result in the loss of pending updates -- an explicit flush invocation is not required upon termination to ensure that pending updates are made persistent.". Users of the Preferences API do not expect to have to flush(), so I had to add an automatic flush to the update methods.

License

CC0

The person who associated a work with this deed has dedicated the work to the public domain by waiving all of his or her rights to the work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law.

Attribution is appreciated but not required.

Full terms.

Code

package net.infotrek.util.prefs;
 
import java.io.File;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import java.util.prefs.PreferencesFactory;
import java.util.prefs.BackingStoreException;
 
/**
 * PreferencesFactory implementation that stores the preferences in a user-defined file. To use it,
 * set the system property <tt>java.util.prefs.PreferencesFactory</tt> to
 * <tt>net.infotrek.util.prefs.FilePreferencesFactory</tt>
 * <p/>
 * The file defaults to [user.home]/.fileprefs, but may be overridden with the system property
 * <tt>net.infotrek.util.prefs.FilePreferencesFactory.file</tt>
 *
 * @author David Croft (<a href="http://www.davidc.net">www.davidc.net</a>)
 * @version $Id: FilePreferencesFactory.java 282 2009-06-18 17:05:18Z david $
 */
public class FilePreferencesFactory implements PreferencesFactory
{
  private static final Logger log = Logger.getLogger(FilePreferencesFactory.class.getName());
 
  Preferences rootPreferences;
  public static final String SYSTEM_PROPERTY_FILE =
    "net.infotrek.util.prefs.FilePreferencesFactory.file";
 
  public Preferences systemRoot()
  {
    return userRoot();
  }
 
  public Preferences userRoot()
  {
    if (rootPreferences == null) {
      log.finer("Instantiating root preferences");
 
      rootPreferences = new FilePreferences(null, "");
    }
    return rootPreferences;
  }
 
  private static File preferencesFile;
 
  public static File getPreferencesFile()
  {
    if (preferencesFile == null) {
      String prefsFile = System.getProperty(SYSTEM_PROPERTY_FILE);
      if (prefsFile == null || prefsFile.length() == 0) {
        prefsFile = System.getProperty("user.home") + File.separator + ".fileprefs";
      }
      preferencesFile = new File(prefsFile).getAbsoluteFile();
      log.finer("Preferences file is " + preferencesFile);
    }
    return preferencesFile;
  }
 
  public static void main(String[] args) throws BackingStoreException
  {
    System.setProperty("java.util.prefs.PreferencesFactory", FilePreferencesFactory.class.getName());
    System.setProperty(SYSTEM_PROPERTY_FILE, "myprefs.txt");
 
    Preferences p = Preferences.userNodeForPackage(my.class);
 
    for (String s : p.keys()) {
      System.out.println("p[" + s + "]=" + p.get(s, null));
    }
 
    p.putBoolean("hi", true);
    p.put("Number", String.valueOf(System.currentTimeMillis()));
  }
}

package net.infotrek.util.prefs;
 
import java.util.*;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.util.prefs.AbstractPreferences;
import java.util.prefs.BackingStoreException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.FileOutputStream;
 
/**
 * Preferences implementation that stores to a user-defined file. See FilePreferencesFactory.
 *
 * @author David Croft (<a href="http://www.davidc.net">www.davidc.net</a>)
 * @version $Id: FilePreferences.java 283 2009-06-18 17:06:58Z david $
 */
public class FilePreferences extends AbstractPreferences
{
  private static final Logger log = Logger.getLogger(FilePreferences.class.getName());
 
  private Map<String, String> root;
  private Map<String, FilePreferences> children;
  private boolean isRemoved = false;
 
  public FilePreferences(AbstractPreferences parent, String name)
  {
    super(parent, name);
 
    log.finest("Instantiating node " + name);
 
    root = new TreeMap<String, String>();
    children = new TreeMap<String, FilePreferences>();
 
    try {
      sync();
    }
    catch (BackingStoreException e) {
      log.log(Level.SEVERE, "Unable to sync on creation of node " + name, e);
    }
  }
 
  protected void putSpi(String key, String value)
  {
    root.put(key, value);
    try {
      flush();
    }
    catch (BackingStoreException e) {
      log.log(Level.SEVERE, "Unable to flush after putting " + key, e);
    }
  }
 
  protected String getSpi(String key)
  {
    return root.get(key);
  }
 
  protected void removeSpi(String key)
  {
    root.remove(key);
    try {
      flush();
    }
    catch (BackingStoreException e) {
      log.log(Level.SEVERE, "Unable to flush after removing " + key, e);
    }
  }
 
  protected void removeNodeSpi() throws BackingStoreException
  {
    isRemoved = true;
    flush();
  }
 
  protected String[] keysSpi() throws BackingStoreException
  {
    return root.keySet().toArray(new String[root.keySet().size()]);
  }
 
  protected String[] childrenNamesSpi() throws BackingStoreException
  {
    return children.keySet().toArray(new String[children.keySet().size()]);
  }
 
  protected FilePreferences childSpi(String name)
  {
    FilePreferences child = children.get(name);
    if (child == null || child.isRemoved()) {
      child = new FilePreferences(this, name);
      children.put(name, child);
    }
    return child;
  }
 
 
  protected void syncSpi() throws BackingStoreException
  {
    if (isRemoved()) return;
 
    final File file = FilePreferencesFactory.getPreferencesFile();
 
    if (!file.exists()) return;
 
    synchronized (file) {
      Properties p = new Properties();
      try {
        p.load(new FileInputStream(file));
 
        StringBuilder sb = new StringBuilder();
        getPath(sb);
        String path = sb.toString();
 
        final Enumeration<?> pnen = p.propertyNames();
        while (pnen.hasMoreElements()) {
          String propKey = (String) pnen.nextElement();
          if (propKey.startsWith(path)) {
            String subKey = propKey.substring(path.length());
            // Only load immediate descendants
            if (subKey.indexOf('.') == -1) {
              root.put(subKey, p.getProperty(propKey));
            }
          }
        }
      }
      catch (IOException e) {
        throw new BackingStoreException(e);
      }
    }
  }
 
  private void getPath(StringBuilder sb)
  {
    final FilePreferences parent = (FilePreferences) parent();
    if (parent == null) return;
 
    parent.getPath(sb);
    sb.append(name()).append('.');
  }
 
  protected void flushSpi() throws BackingStoreException
  {
    final File file = FilePreferencesFactory.getPreferencesFile();
 
    synchronized (file) {
      Properties p = new Properties();
      try {
 
        StringBuilder sb = new StringBuilder();
        getPath(sb);
        String path = sb.toString();
 
        if (file.exists()) {
          p.load(new FileInputStream(file));
 
          List<String> toRemove = new ArrayList<String>();
 
          // Make a list of all direct children of this node to be removed
          final Enumeration<?> pnen = p.propertyNames();
          while (pnen.hasMoreElements()) {
            String propKey = (String) pnen.nextElement();
            if (propKey.startsWith(path)) {
              String subKey = propKey.substring(path.length());
              // Only do immediate descendants
              if (subKey.indexOf('.') == -1) {
                toRemove.add(propKey);
              }
            }
          }
 
          // Remove them now that the enumeration is done with
          for (String propKey : toRemove) {
            p.remove(propKey);
          }
        }
 
        // If this node hasn't been removed, add back in any values
        if (!isRemoved) {
          for (String s : root.keySet()) {
            p.setProperty(path + s, root.get(s));
          }
        }
 
        p.store(new FileOutputStream(file), "FilePreferences");
      }
      catch (IOException e) {
        throw new BackingStoreException(e);
      }
    }
  }
}

dalai-lama

Sat, 20/06/2009 - 00:05

I believe the documentation that states

"Normal termination of the Java Virtual Machine will not result in the loss of pending updates -- an explicit flush invocation is not required upon termination to ensure that pending updates are made persistent.". Users of the Preferences API do not expect to have to flush(), so I had to add an automatic flush to the update methods."

means for the user of the Preferences API, this is true. Since you are providing an implementation of the Preferences backing storage, you as the implementer must ensure that these things are true. For example, the file system backing storage implementation that is used on *nix systems, sets up a timer to go off to call sync periodically and hook to ensure that flush is called if the JVM terminates normally. You could do the same in your implementation.

Funny I was looking at what sync and flush are supposed to do and when they are supposed to do it. I just finished (about an hour ago) an implementation of the Preferences backing storage that uses JPA to store the preferences to a database. This is used in a J2EE web application and I was wondering when flush was going to be called or when sync was going to be needed. My current implementation, just ignores these calls as I am using JPA to persist and retrieve the nodes, keys, and values. So my backing storage is always in sync as long as the same web application is using the properties. I think I might go back in and fix the sync to handle database changes made "behind JPA's back".

Anyways, nice article and the timing for me to find this is quite a coincidence!

Brett