by Ray Djajadinata

Sir, what is your preference?

news
Aug 31, 200117 mins

Manage your application's preferences with J2SE 1.4's new Preferences API

Almost all but the smallest applications have preferences — a set of values that affect how the application behaves at runtime. Preferences examples range from configuring an MP3 player to sport a skin of your choice, to setting a 3D shooter game’s to varying screen resolutions. Now, of course you want the values to be there next time you run the application, so usually they’re made persistent, be it in a file, database, or some other storage mechanism.

As an application developer, how and where you put the preferences vary depending on your application, the platform it runs on, and the programming language. If your application will run only on Windows, then Registry is the place to put the preferences. Developers writing applications written in portable C/C++, on the other hand, usually put their preferences in files. Bigger, server-side applications might store them in a database (although usually something needs to be stored in a file — the connection string to the database, for example).

Therefore, when designing your application, you’ll always face the same preference management questions: Where do we store them? How? Is maintaining them easy enough? Can we easily move them to another platform when necessary? If Java is your programming language, you can follow several approaches in answering these questions. I’ll compare the traditional approaches, then discuss in detail the latest option — J2SE (Java 2, Standard Edition) 1.4’s new Preferences API. Enjoy!

The old, simple, and unscalable approach

Ahhh — good old Properties. It has been around since Java’s beginning, JDK 1.0 to be exact. It’s easy to use: just put the preferences into the Properties object and store them in a file. Later, when you need to get them back, load it from the file, and voila, you have a Properties object containing the values you previously stored in the file. It’s as simple as that. What could possibly be wrong with this approach?

The problems with Properties

For simple applications with just a few preferences, java.util.Properties works just fine. However, as the application grows in size, with the Properties API you can develop problems such as:

  • Numerous property files, each assigned to a part of the application (usually, but not always, a package), forcing you to hardcode a gaggle of file names — or even their paths — inside your code
  • Several large property files, which force you to prevent name collisions and to track numerous different preferences

To solve the second problem, you’d typically create your own hierarchy out of the flat namespace. That is, you’d create names like:

  • myapp.payment.SSLPort=10256
  • myapp.payment.SETPort=10257
  • myapp.admin.listenPort=10258

Of course, the Properties API possesses no awareness of this hierarchy, so you’d have to add your own code on top of it. However, this approach also proves unsafe: if someone hand-edits the file and replaces a period with a comma in one of the names above, who knows what will happen to your application?

In other words, managing and maintaining preferences using the Properties API can quickly become a nightmare — it simply doesn’t scale! Besides, using the Properties API this way won’t work with a platform without a local disk. Now let’s take a look at another approach that solves most of these problems.

The powerful, back-end neutral, and (too) complex approach

In the second approach, you store preferences in a directory service through the JNDI (Java Naming Directory Interface) API, thus solving most problems that Properties presents. JNDI-stored preferences, however, generally proves too complex for our purposes. Not all applications designers want anything to do with a directory service; most just need a simple yet elegant way to store preferences.

Besides, you still don’t know where exactly in the namespace you should store the preference data. Ideally, you don’t want to know where; you just want a place to stuff the data in and get them out when needed!

The new, improved, yet simple approach!

The Preferences API solves problems the two approaches above cause, while keeping the interface simple. The whole package comprises only three interfaces, four classes, and two exceptions. Even better, most of the time, you need care about only one class: Preferences. (Other classes are more relevant to the API’s implementers than its users.)

Because the Preferences API is back-end neutral, you need not care whether the data are stored in files, database tables, or a platform-specific storage such as the Windows Registry. In fact, you don’t want to care, because the situation may differ across varying platforms, or even across implementations on the same platform.

The Preferences API also offers a simple way to organize your preferences hierarchically and to associate a package with a node. Both prove convenient because organizing your preferences becomes a no-brainer: a set of preferences for a module becomes associated with the respective package, that’s it. You don’t have to think, “Oh, let’s see, for this module, they have to go to this file, and that module, that file, or is it this file? What’s the convention again?”

I once enthusiastically told a colleague about the Preferences API, quite sure that he’d be impressed. Expressionless, he listened to me for a while, then asked:

Do you always have to go through the API to access the preferences? We move preferences from platform to platform during testing, and copying the property files over has worked well so far.

Well, I learned two things from his remark: first, impressing colleagues is not easy. Second, the Preferences API does offer an easy solution to his problem. That is, you can export the whole preferences tree, or just a node to XML format. Copy this file to the other machine, import it back, and you don’t have to reset all the preferences from scratch!

Some basic concepts

By now you know that with the Preferences API you can organize your preferences in a hierarchical manner. Not only that, it also recognizes that some preferences are shared among users, while some are not.

Trees, trees everywhere

In the Preferences API, you manage preferences in tree-like collections of nodes, which act similarly to directories in a hierarchical file system. The name-value pairs under the nodes, in turn, act roughly similar to files under the directories.

Moreover, you traverse a preference tree similarly to the way you’d traverse a directory tree. At the top of a tree resides a root node with an absolute path name of "/". Accordingly, a node named A under this root node has an absolute path name of "/A", and a node named B under A "/A/B", and so on.

System and user preference trees

The Preferences API recognizes two kinds of preference tree: one shared by all users of the system (that is, the system preference tree), and another specific to a particular user (the user preference tree). The notion of “system” and “user,” however, can vary across implementations. For more details, read the sidebar, “Platform Differences.”

Put it into use

OK, enough talk about concepts. How do you use the API? The following sections show you how!

Obtain a Preferences object

Of course, before everything else, you have to obtain a Preferences object to work with. Unlike Properties, Preferences doesn’t have any accessible constructor. Instead, you obtain the object through one of four static factory methods:

  • public static Preferences systemNodeForPackage(Object o): Returns a node in the system preferences tree corresponding to the passed object’s package
  • public static Preferences userNodeForPackage(Object o): Returns a node in the user preferences tree corresponding to the passed object’s package
  • public static Preferences systemRoot(): Returns the root of the system preferences tree
  • public static Preferences userRoot(): Returns the root of the user preferences tree

In the list above, the first two methods serve as convenience methods. Let’s say you have class C in package P. Passing an instance of C to either of them will return (and if necessary, create) a node whose path corresponds to package P. So, even if the package name changes for some reason, there’s no need to change your code. Listing 1 illustrates this point further:

Listing 1. Payer.java

package com.acme.myapp.user;
import java.util.prefs.Preferences;
public class Payer {
   private static final String PREFERRED_CHANNEL = 
      "PreferredChannel";
   private PreferredChannel preferredChannel = 
      PreferredChannel.WML;
   public void storePreferences() {
      Preferences prefs = Preferences.userNodeForPackage(this);
      prefs.put(PREFERRED_CHANNEL, preferredChannel.toString());
      // The rest of the code here...
   }
   public static void main(String[] args) {
      // The test code
      new Payer().storePreferences();
   }
}

You’ll see the result of running the code above under J2SE Beta 1.4 for Windows in Figure 1. Note that WML is encoded as /W/M/L, and PreferredChannel as /Preferred/Channel because Preferences has case-sensitive keys and node-names, while the backing store of choice (in this case Windows Registry) does not.

The last two factory methods — systemRoot() and userRoot() — simply return the system root and the user root, respectively. This might be useful when you don’t have any instance to pass in (in static methods, for example), or when you need to create paths manually (that is, independent of the package where the code resides). However, systemRoot() and userRoot() merely return the root nodes. To create and traverse down the branches of the preference tree, you need another method:

public abstract Preferences node(String pathName)

node() accepts either an absolute (begins with a “/“) or a relative path name. Note: a Preferences object interprets the latter relative to the node the object corresponds to. That is, if you do this:

Preferences prefs = Preferences.systemRoot().node("/the/first/node");
Preferences prefs2 = prefs.node("the/second/node");
Preferences prefs3 = prefs.node("/yet/another/branch");

you’ll get what appears in Figure 2.

Figure 2. The node() method in action

Get and put preferences values

Unlike Properties, Preferences doesn’t force you to convert everything into String before putting them in (and likewise, it doesn’t force you to retrieve everything as String). Instead, it offers methods in get/put pairs for each of the following types:

  • String
  • boolean
  • byte[]
  • double
  • float
  • int
  • long

However, unlike Properties, Preferences does force you to provide a default value whenever you call one of the get methods. The rationale behind this is that the application should still run even though the backing store is unavailable, so you should provide a reasonable default value if for some reason the previously stored value is irretrievable.

Don’t call us, we’ll call you

The Observer pattern is a bit like the so-called Hollywood Principle: “Don’t call us, we’ll call you.” I guess aspiring movie stars hate it, but don’t we lazy programmers just love it! We’re too lazy to check every once in a while whether something has changed, aren’t we? We just want notification when it actually happens!

Preferences lets us achieve just what we want: by registering a NodeChangeListener to a node, you’ll get notified whenever a child node under this node is added or removed. Not only that, you can also listen for notifications when a preference is added to or removed from the node, or when the value of the preference changes. Let’s see the listeners in action in Listing 2:

Listing 2. TestTheListeners.java

import java.util.prefs.*;
public class TestTheListeners {
   public static void main(String[] args) {
      MyListener listener = new MyListener();
      // Create a node for testing 
      Preferences listeningNode = Preferences.userRoot().node("listening");
      listeningNode.addNodeChangeListener(listener);
      listeningNode.addPreferenceChangeListener(listener);
      try {
         listeningNode.put("key1", "bar");
         listeningNode.putInt("key1", 8276);
         listeningNode.clear();
         Preferences test1 = listeningNode.node("test1");
         Preferences test2 = test1.node("test2");
         test2.removeNode();
         test1.removeNode();
      } catch(BackingStoreException bsEx) {
         // Ignore it
      }
      try {
         Thread.sleep(5000);
      } catch(InterruptedException iEx) {
         // Ignore it as well
      }
   }
}
class MyListener implements NodeChangeListener, PreferenceChangeListener {
   public void childAdded(NodeChangeEvent nceEvt) {
      System.out.print("Child: ");
      System.out.print(nceEvt.getChild().name());
      System.out.print(" added to Parent: ");
      System.out.println(nceEvt.getParent().name());
   }
   public void childRemoved(NodeChangeEvent nceEvt) {
      System.out.print("Child: ");
      System.out.print(nceEvt.getChild().name());
      System.out.print(" removed from Parent: ");
      System.out.println(nceEvt.getParent().name());
   }
   public void preferenceChange(PreferenceChangeEvent pceEvt) {
      System.out.print("Preference change at Node: ");
      System.out.print(pceEvt.getNode().name());
      System.out.print(" key: " + pceEvt.getKey());
      System.out.println(" value: " + pceEvt.getNewValue());
   }
}

Running this produces the following result:

Preference change at Node: listening key: key1 value: bar
Preference change at Node: listening key: key1 value: 8276
Preference change at Node: listening key: key1 value: null
Child: test1 added to Parent: listening
Child: test1 removed from Parent: listening

Consider these items regarding listening to changes in the preferences tree:

  • A particular listener listens only at the node with which it is registered. Meaning, no matter how many nodes farther down the tree are removed or added, as long as the nodes directly under this node remain unchanged, no event will generate.
  • Events are only guaranteed for changes made within the same JVM as the registered listener. That is, even if application A has a listener registered at node N, no guarantee exists that it will receive an event for changes made to node N by application B running within another JVM.
  • It is possible to get an event before the causing change has been made permanent. The implementation does not sync() with the backing store for every node or preference-changing operation (for good reasons: with some backing stores, it can produce a huge performance hit).

Import/Export: an easy way to move preferences around

Let’s face it: the Properties API is sooo simple, easy, and convenient to use. Once you’ve stored the preferences in a file, you can hand-edit them, zip, then ftp or email them so your poor, freezing colleague in the data center can get your application running with the correct settings and get out of there as soon as possible.

The Preferences API also offers this convenience feature (which is not always a good thing, actually: careless hand-editing does happen, and the resulting bug can be hard to debug! In any case, it’s there if you need it.). The API exports either a node or the whole subtree into XML, and is able to import the same format back into the backing store. Listing 3 shows exporting preferences into files:

Listing 3. PreferencesExporter.java

package org.acme.testexim;
import java.util.prefs.*;
import java.io.*;
public class PreferencesExporter {
   private static final String PACKAGE = "/org/acme/testexim";
   public static void main(String[] args) {
      doThings(Preferences.systemRoot().node(PACKAGE));
      doThings(Preferences.userRoot().node(PACKAGE));
   }
   public static void doThings(Preferences prefs) {
      prefs.putBoolean("Key0", false);
      prefs.put("Key1", "Value1");
      prefs.putInt("Key2", 2);
      Preferences grandparentPrefs = prefs.parent().parent();
      grandparentPrefs.putDouble("ParentKey0", Math.E);
      grandparentPrefs.putFloat("ParentKey1", (float)Math.PI);
      grandparentPrefs.putLong("ParentKey2", Long.MAX_VALUE);
      String fileNamePrefix = "System";
      if(prefs.isUserNode()) {
         fileNamePrefix = "User";
      }
      try {
         OutputStream osTree = 
            new BufferedOutputStream(
             new FileOutputStream(fileNamePrefix + "Tree.xml"));
         grandparentPrefs.exportSubtree(osTree); 
         osTree.close();
         
         OutputStream osNode = 
            new BufferedOutputStream(
             new FileOutputStream(fileNamePrefix + "Node.xml"));
         grandparentPrefs.exportNode(osNode);
         osNode.close();
      } catch(IOException ioEx) {
         // ignore
      } catch(BackingStoreException bsEx) {
         // ignore too
      }
   }
}

When run, PreferencesExporter generates four files:

  • SystemNode.xml
  • SystemTree.xml
  • UserNode.xml
  • UserTree.xml

Let’s take a closer look at two of them, SystemNode.xml and UserTree.xml:

Listing 4. SystemNode.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE preferences SYSTEM 'http://java.sun.com/dtd/preferences.dtd'>
<preferences EXTERNAL_XML_VERSION="1.0">
  <root type="system">
    <map />
    <node name="org">
      <map>
        <entry key="ParentKey0" value="2.718281828459045" />
        <entry key="ParentKey1" value="3.1415927" />
        <entry key="ParentKey2" value="9223372036854775807" />
      </map>
    </node>
  </root>
</preferences>

And here’s the code for UserTree.xml:

Listing 5. UserTree.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE preferences SYSTEM 'http://java.sun.com/dtd/preferences.dtd'>
<preferences EXTERNAL_XML_VERSION="1.0">
  <root type="user">
    <map />
    <node name="org">
      <map>
        <entry key="ParentKey0" value="2.718281828459045" />
        <entry key="ParentKey1" value="3.1415927" />
        <entry key="ParentKey2" value="9223372036854775807" />
      </map>
      <node name="acme">
        <map />
        <node name="testexim">
          <map>
            <entry key="Key0" value="false" />
            <entry key="Key1" value="Value1" />
            <entry key="Key2" value="2" />
          </map>
        </node>
      </node>
    </node>
  </root>
</preferences>

From the two files above we can see the difference between exportNode() and exportSubtree(), as well as that the exported XML from the user preference tree contains no information about the calling user. This means Preferences cannot know which user the exported file comes from, so if you export user A’s preferences and import them with user B as the calling user, they will import into user B’s preference tree. This situation can be bad or good, depending on your needs.

Importing preferences, of course, is a piece of cake:

InputStream is = new BufferedInputStream(
   new FileInputStream(args[0]));
Preferences.importPreferences(is);
is.close();

Change the Preferences implementation

The Preferences API designers wrote it such that it is possible to use different Preferences implementations. How to specify which implementation to use, however, is not part of the specification (read: may change in future releases). At the time of this writing, in Sun’s J2SE 1.4 Beta you specify the implementation by setting the system property java.util.prefs.PreferencesFactory, giving the fully qualified name of the implementing class.

So let’s say you wrote your own Preferences implementation with PostGreSQL as the backing store, and the name of your preferences factory is com.acme.prefs.PostGreSQLPreferencesFactory. In this situation, switching Preferences implementations proves quite simple. Just add the following to the usual command line when launching the application:

-Djava.util.prefs.PreferencesFactory=
   com.acme.prefs.PostGreSQLPreferencesFactory

Interesting avenues for further exploration

In this article, you’ve seen what the Preferences API can do to simplify your task of making design decisions regarding application preference management. You’ve learned what problems can arise with Properties and JNDI-stored properties. Obtaining the Preferences object, getting and putting preference values, registering listeners to monitor the changes in nodes and preferences, importing and exporting preferences, and switching implementations — we’ve covered them all.

So, is that it? No! Based on what you’ve learned from this article, it is not difficult to come up with your own cross-platform, back-end neutral Registry Editor that provides easy access to preferences trees on any platform — now that surely beats hand-editing Properties files!

Be sure to find out exactly what the API does and doesn’t support so you won’t be surprised when using it. For instance, Preferences doesn’t support atomic update spanning multiple preferences. This requires the values to be compounded into a single preference value, which means that you have to come up with your own encoding to achieve atomicity. Check the Resources section for more details.

Last but not least, if you’re lucky (or maybe you’ll call that unlucky), you might get to write your own Preferences implementation. Now that’s something I’ll call the ultimate challenge in this subject!

Ray Djajadinata first encountered Java in 1995 in its launching seminar. He then wrote introductory Java articles in a local programming magazine and explored the language further, while still using C++ at work. In 1999, he got the opportunity to use Java at work as well, and he’s been hooked ever since! At the time of this writing, he’s a senior software engineer at Netlife Solutions, a company of EdgeMatrix. He’s a Sun Certified Java Programmer, and planning to get certified as Architect and Developer too, as his current job seems to be the mix of those three.