IBM Skip to main content
Search for:   within 
      Search help  
     IBM home  |  Products & services  |  Support & downloads   |  My account

developerWorks > Java technology
developerWorks
Store objects using the Preferences API
code73 KBe-mail it!
Contents:
Why design the Preferences API?
Using Preferences
Getting a value
Getting the node for a package
Storing objects
Turning objects into byte arrays
Breaking objects into pieces
Reading and writing the pieces
Putting it all together
Storing the information away
Resources
About the author
Rate this article
Related content:
Magic with Merlin: Working with preferences
IBM developer kits for Java (downloads)
Subscriptions:
dW newsletters
dW Subscription
(CDs and downloads)
If your data can be expressed as simple objects, the API can be useful storage

Level: Introductory

Greg Travis (mailto:mito@panix.com?cc=&subject=Store objects using the Preferences API)
Freelance programmer
14 October 2003

The Preferences API -- a lightweight, cross-platform persistence API introduced in JDK 1.4 -- is designed to store small amounts of data (string, simple byte arrays, and so on.) and was not intended to be an interface to a traditional database. It can, however, be effective as a storage device if your data can be expressed as simple objects. This article offers an introduction to the API, explains how objects are stored, demonstrates the process in action, and provides a code library to do the work.

The Preferences API is a lightweight, cross-platform persistence API that was introduced in JDK 1.4. It was not meant to interface to traditional database engines, but rather uses appropriate OS-specific back ends for the actual persistence. The API was meant for storing small amounts of data. In fact, the name itself indicates this: a common use for it is to store user-specific settings, or preferences, such as font size or window layout. (Of course, you can store anything you want in it.)

The Preferences API is designed to store strings, numbers, booleans, simple byte arrays, and the like. In this article, we will show you how to store objects using the Preferences API and provide a working library that takes care of the details for you. This is useful if your data is easily expressed as simple objects, rather than as separate values like strings and numbers.

We'll begin with a brief discussion about the API, including some simple examples of its use, then go into the details of how objects are stored using the API and lay out the code that takes care of this for us. We also demonstrate some examples of the API in action.

Why design the Preferences API?
It would be surprising if the Preferences API was created mainly to allow Java programs to access the Microsoft Windows registry. Why do I mention this? The design of the API is similar to that of the Windows registry; most of the statements in the first three paragraphs of this article apply equally well to the registry.

However, the Preferences API, like all of the Java language, is intended to be cross-platform, so it works at least as well on non-Windows systems. (The code in this article, of course, is cross-platform.)

The specification for the Preferences API doesn't specify how the API must be implemented, but only what it must do. Each implementation of the Java Runtime Environment (JRE) can implement the API differently. Many non-registry implementations store API data in an XML-formatted file, either in the user's home directory or in a shared directory.

Like the Windows registry, the Preferences API uses a hierarchical tree metaphor to store data. The starting point is a root node (the root node is the base of the tree; all other nodes are descendents of this node). Nodes can contain named values, as well as other nodes. Different programs store their data in different parts of the tree, so they won't conflict with each other. As we'll see, the Preferences API takes special measures to help prevent such conflicts.

We'll start by taking a quick look at how the Preferences API works and how it is used.

Using Preferences
The best way to understand the Preferences API is to use it. The first thing you need to do is to get access to the root node:


Preferences root = Preferences.userRoot();

This line of code returns the user root of the data tree. Previously, we said that all of the data in the system is stored in a tree. Well, this isn't exactly true -- there are, in fact, two data trees -- the user tree and the system tree. The two trees behave identically, but they have different purposes. The system tree is used to store data that is intended to be available to all users, while the user tree is different for each user.

These two trees naturally have different purposes. You would store a font preference in the user tree, as that is a user-specific issue. On the other hand, you would store the location of a program in the system tree, as that location is the same for all users and is potentially useful to all users.

Smaller programs will use either the system tree or the user tree, but not both. Larger applications might use both. In this article, we will address the user tree only, keeping in mind that the user and system trees behave identically.

Now let's see how to read and write simple values using the Preferences API.

Getting a value
After you have your root node, you use it to read and write values. Here's how you might write a font size:


root.putInt( "fontsize", 10 );

And here's how you read it back later:


int fontSize = prefs.getInt( "fontsize", 12 );

Note that getInt() requires a default value -- in this case, 12.

Of course, you can read and write more than just integers. You can read and write many primitive Java types. You can also store nodes inside other nodes, as this example demonstrates:


Preferences child = parent.node( "child" );

That's all there is to the Preferences API -- the remainder of the usefulness is in the details, one of which we'll look at in the next section.

Getting the node for a package
It's not hard to imagine that two different programmers might want to store different font sizes, and if they each decided to store theirs under the name "font size," we'd have a problem. The preferences for one program would infect the other program.

The solution is to store things in package-specific locations, like so:


Preferences ourRoot = Preferences.userNodeForPackage( getClass() );

The userNodeForPackage() method takes a Class object and returns the node specific to that class. This way, each application -- which presumably is in its own package -- will have its own preferences node.

Now that we have a good idea of how the Preferences API works, we need to know how to extend it to be able to handle objects.

Storing objects
Ideally, this is how we'd like to be able to write an object into the Preferences tree:

Listing 1. The ideal way to write objects into the Preferences tree

Font font = new Font( ... );
Preferences prefs = Preferences.userNodeForPackage( getClass() );
prefs.putObject( "font", font );

Sadly, however, the Preferences object does not have the putObject() and getObject() methods. But we're going to come as close as we can to this. We're going to implement these methods in a class called PrefObj. Here's how it works:

Listing 2. Implementing putObject() and getObject()

Font font = new Font( ... );
Preferences prefs = Preferences.userNodeForPackage( getClass() );
PrefObj.putObject( prefs, "font", font );

It's as close as we're going to be able to get to adding methods to the Preferences class.

In the next section, we'll see how getObject() and putObject() are implemented.

Turning objects into byte arrays
The technique we use here involves two tricks. The first trick is to turn an object into a byte array. The reason for doing this is simple: although the Preferences object does not handle objects, it does handle byte arrays.

Luckily, this isn't something we have to do from scratch -- it's built into the Java language. There are several ways to convert objects into byte arrays; here's how we do it in the PrefObj class:

Listing 3. Converting objects to byte arrays

static private byte[] object2Bytes( Object o ) throws IOException {
  ByteArrayOutputStream baos = new ByteArrayOutputStream();
  ObjectOutputStream oos = new ObjectOutputStream( baos );
  oos.writeObject( o );
  return baos.toByteArray();
}

The ObjectOutputStream class is essential here -- it's what does the magic of actually turning an object into a stream of bytes. By wrapping the ObjectOutputStream around a ByteArrayOutputStream, we can turn the byte stream into a byte array.

There's also a method that goes the other way:

Listing 4. Converting byte arrays to objects

static private Object bytes2Object( byte raw[] )
    throws IOException, ClassNotFoundException {
  ByteArrayInputStream bais = new ByteArrayInputStream( raw );
  ObjectInputStream ois = new ObjectInputStream( bais );
  Object o = ois.readObject();
  return o;
}

It's important to remember that ObjectOutputStream only handles objects that implement the java.io.Serializable interface. Fortunately, this includes almost all of the objects in the core Java libraries, as well as any objects from your program that you have declared as implementing Serializable.

As I mentioned earlier, the Preferences API does handle byte arrays. However, the byte arrays we've constructed here aren't quite right, as we'll see in the next section.

Breaking objects into pieces
The Preferences API imposes a limit on the size of the data that you can store in it. In particular, strings are limited to MAX_VALUE_LENGTH characters. Byte arrays are limited in length to 75 percent of MAX_VALUE_LENGTH because byte arrays are stored by encoding them as strings.

An object, on the other hand, can be of arbitrary size, so we need to break it into pieces. Of course, the easiest way to do this is to first convert it to a byte array and then break the byte array into pieces. Here's the code for breaking down a byte array, again from PrefObj:

Listing 5. Breaking byte arrays into digestible chunks

static private byte[][] breakIntoPieces( byte raw[] ) {
  int numPieces = (raw.length + pieceLength - 1) / pieceLength;
  byte pieces[][] = new byte[numPieces][];
  for (int i=0; i<numPieces; ++i) {
    int startByte = i * pieceLength;
    int endByte = startByte + pieceLength;
    if (endByte > raw.length) endByte = raw.length;
    int length = endByte - startByte;
    pieces[i] = new byte[length];
    System.arraycopy( raw, startByte, pieces[i], 0, length );
  }
  return pieces;
}

There's nothing complicated here -- we just create an array of arrays, each of which is at most pieceLength bytes long (pieceLength being three-fourths of MAX_VALUE_LENGTH). Correspondingly, there's another method to put the pieces back together again:

Listing 6. Reassembling pieces into the whole byte array

static private byte[] combinePieces( byte pieces[][] ) {
  int length = 0;
  for (int i=0; i<pieces.length; ++i) {
    length += pieces[i].length;
  }
  byte raw[] = new byte[length];
  int cursor = 0;
  for (int i=0; i>pieces.length; ++i) {
    System.arraycopy( pieces[i], 0, raw, cursor, pieces[i].length );
    cursor += pieces[i].length;
  }
  return raw;
}

This routine checks the total length of the pieces and creates a new, single array that is that long. Then it copies the pieces in, one after another.

Reading and writing the pieces
At this stage we employ our second trick -- turning a value into a node. Generally, when we store a value using the Preferences API, we put it in a slot in one of the nodes in the preferences data tree.

But we can't really do that here. Even though an object is a single value, we've turned it into a set of fixed-length byte arrays. If we had only one byte array, it would be easy to write to a slot in the data tree, since the Preferences API supports byte arrays directly. But that won't work because we have multiple arrays.

The trick is to allocate a node for each object. Let's make sure we are clear about what this means.

Normally, you store a value in a slot that exists among other slots in a node. But we're going to create a node for each object and store the byte arrays in the slots of that node. Let's put this into more concrete terms. If we could, we would store an object into a single slot:

Listing 7. Store an object in a single slot

Preferences parent = ....;
parent.putObject( "child", object );

But we can't do that because Preferences does not have a putObject() method. Instead, we create a node and store the byte arrays in it, as shown here:

Listing 8. Instead, store the byte arrays in a node

Preferences parent = ....;
Preferences child = parent.node( "child" );
for (int i=0; i<pieces.length; ++i) {
  child.putByteArray( ""+i, pieces[i] );
}

Thus, instead of storing a single value in the slot called "child", we are storing several values in a node called "child." These values are stored using numerical keys -- "0," "1," "2," and so on.

Using numerical keys makes it easy to read the pieces later:

Listing 9. Reading the easy-to-read pieces

Preferences parent = ....;
Preferences child = parent.node( "child" );
for (int i=0; i<numPieces; ++i) {
  pieces[i] = child.getByteArray( ""+i, null );
}

In the next section, we'll take a look at the routines that combine all of these steps.

Putting it all together
PrefObjs has a static method called putObject(), which calls the methods described earlier in Listings 3, 5, and 8. It looks like this:

Listing 10. Method putObject() uses other methods to write pieces

static public void putObject( Preferences prefs, String key, Object o )
    throws IOException, BackingStoreException, ClassNotFoundException {
  byte raw[] = object2Bytes( o );
  byte pieces[][] = breakIntoPieces( raw );
  writePieces( prefs, key, pieces );
}

Method putObject() breaks the entire process into three steps, embodied by three methods we discussed previously. It converts the object to a byte array (Listing 3), breaks the array into smaller arrays (Listing 5), and then writes the pieces to the Preferences API.

There is a similar method for reading:

Listing 11. Method getObject() does the same for reading pieces

static public Object getObject( Preferences prefs, String key )
    throws IOException, BackingStoreException, ClassNotFoundException {
  byte pieces[][] = readPieces( prefs, key );
  byte raw[] = combinePieces( pieces );
  Object o = bytes2Object( raw );
  return o;
}

This method reads the pieces from the Preferences API, combines them into a single byte array, and then converts this back into an object.

Storing the information away
As you can see, this is a clean way to use the features the Preferences API has, to implement the features that it doesn't have. This is a good way to extend an existing library. In theory, you could hack the library or try to create a subclass, but such approaches might break other programs that use the Preferences API. With this approach, you keep the original API intact while extending it in a clean, useful way.

Resources

About the author
Greg Travis is a freelance Java programmer and technology writer living in New York City. Greg started his programming career in 1992, spending three years in the world of high-end PC games. In 1995, he joined EarthWeb, where he began developing new technologies with the Java programming language. Since 1997, Greg has been a consultant in a variety of Web technologies, specializing in real-time graphics and sound. His interests include algorithm optimization, programming language design, signal processing (with emphasis on music), and real-time 3D graphics. He is also the author of JDK 1.4 Tutorial, published by Manning Publications. Contact Greg at mito@panix.com


code73 KBe-mail it!

What do you think of this document?
Killer! (5) Good stuff (4) So-so; not bad (3) Needs work (2) Lame! (1)

Comments?



developerWorks > Java technology
developerWorks
  About IBM  |  Privacy  |  Terms of use  |  Contact