|
|
|
Contents: |
|
|
|
Related content: |
|
|
|
Subscriptions: |
|
|
| If your data can be expressed as simple objects, the API can
be useful storage
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
- Download the source code used in
this article.
- "Encrypted Preferences in
Java" focuses on integrating encryption techniques with the
Preferences API. (Site requires registration.)
- "The Preferences API in
Java 2SE 1.4" delivers an overview of the specification.
- "Using the Preferences
API" provides a sample tip on using the API.
- "Magic with Merlin: Working
with preferences" (developerWorks,
October 2001) demonstrates how the Preferences API specification lets
users manipulate user preference data and configuration data by
providing access to an implementation-specific registry.
- The Java Community Process Program provides details on JSR 10: The Preferences
API specification, the simple API that allows programs to manipulate
user preference data and configuration data, available starting in
version 1.4.1.
- The Client-side Java
programming forum covers a few of the topics pertaining to the
Preferences API, including Windows Registry questions and support in
LDAP.
- The Preferences API site
provides an overview to the package, compares it to other similar
mechanisms, and offers notes on its usage and a FAQ on why it was
designed the way it was.
- jGuru offers a quick code answer to the question: "Can you show me a small
snippet of how to use Preferences API in JDK1.4?".
- Chapter 10: "The Preferences API"
(available in PDF) of the author's book, JDK 1.4
Tutorial (Manning Publications, 2002), provides a demonstration
of cross-platform storage of preferences data, including change
listeners.
- A valuable resource for this topic is Java in a Nutshell, 4th
Edition by David Flanagan (O'Reilly & Associates,
2002).
- Find hundreds of Java technology-related resources on the developerWorks Java
technology zone.
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 |
|
|