|
|
The author of this tip is John Zukowski, president and principal
consultant of JZ Ventures, Inc.
The Preferences API was first covered here shortly after it was introduced
with the 1.4 version of the standard platform: the July 15, 2003 article,
the Preferences API.
That article described how to get and set user specific preferences.
There is more to the Preferences API than just getting and setting
user specific settings. There are system preferences, import and export
preferences, and event notifications associated with preferences. There
is even a way to provide your own custom location for storage of
preferences. The first three options mentioned will be described here.
Creating a custom preferences factory will be left to a later tip.
System Preferences
The Preferences API provides for two separate sets of preferences. The
first set is for the individual user, allow multiple users on the same
machine to have different settings defined. These are called user
preferences. Each user who shares the same machine can have his or her
own unique set of values associated with a group of preferences. Something
like this could be like a user password or starting directory. You don't
want every person on the same machine to have the same password and home
directory. Well, I would hope you don't want that.
The other form of preferences is the system type. All users of a machine
share the same set of system preferences. For instance, the location
of an installed printer would typically be a system preference. You
wouldn't necessarily have a different set of printers installed for
different users. Everyone running on one machine would know about all
printers known by that machine.
Another example of a system preference would be the high score of a game.
There should only be one overall high score. That's what a system preference
would be used for. In the previous tip you saw how userNodeForPackge()
-- and subsequently userRoot() -- was used to acquire the user's preference node,
the following example shows how to get the appropriate part of the system
preferences tree with systemNodeForPackage() -- or systemRoot() for the root.
Other than the method call to get the right preference node, the API usage is
identical.
The example is a simple game, using the game term loosely here. It picks
a random number from 0 to 99. If the number is higher than the previously
saved number, it updates the "high score." The example also shows the
current high score. The Preferences API usage is rather simple. The
example just gets the saved value with getSavedHighScore(), providing
a default of -1 if no high score had been saved yet, and
updateHighScore(int value) to store the new high score. The HIGH_SCORE
key is a constant shared by the new Preferences API accesses.
private static int getSavedHighScore() {
Preferences systemNode =
Preferences.systemNodeForPackage(High.class);
return systemNode.getInt(HIGH_SCORE, -1);
}
private static void updateHighScore(int value) {
Preferences systemNode =
Preferences.systemNodeForPackage(High.class);
systemNode.putInt(HIGH_SCORE, value);
}
Here's what the whole program looks like:
import java.util.*;
import java.util.prefs.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class High {
static JLabel highScore = new JLabel();
static JLabel score = new JLabel();
static Random random = new Random(new Date().getTime());
private static final String HIGH_SCORE = "High.highScore";
public static void main (String args[]) {
/* -- Uncomment these lines to clear saved score
Preferences systemNode =
Preferences.systemNodeForPackage(High.class);
systemNode.remove(HIGH_SCORE);
*/
EventQueue.invokeLater(
new Runnable() {
public void run() {
JFrame frame = new JFrame("High Score");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
updateHighScoreLabel(getSavedHighScore());
frame.add(highScore, BorderLayout.NORTH);
frame.add(score, BorderLayout.CENTER);
JButton button = new JButton("Play");
ActionListener listener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
int next = random.nextInt(100);
score.setText(Integer.toString(next));
int old = getSavedHighScore();
if (next > old) {
Toolkit.getDefaultToolkit().beep();
updateHighScore(next);
updateHighScoreLabel(next);
}
}
};
button.addActionListener(listener);
frame.add(button, BorderLayout.SOUTH);
frame.setSize(200, 200);
frame.setVisible(true);
}
}
);
}
private static void updateHighScoreLabel(int value) {
if (value == -1) {
highScore.setText("");
} else {
highScore.setText(Integer.toString(value));
}
}
private static int getSavedHighScore() {
Preferences systemNode =
Preferences.systemNodeForPackage(High.class);
return systemNode.getInt(HIGH_SCORE, -1);
}
private static void updateHighScore(int value) {
Preferences systemNode =
Preferences.systemNodeForPackage(High.class);
systemNode.putInt(HIGH_SCORE, value);
}
}
And, here's what the screen looks like after a few runs. The 61 score is
not apt to be your high score, but it certainly could be.
You can try running the application as different users to see that they all
share the same high score.
Import and Export
In the event that you wish to transfer preferences from one user to another
or from one system to another, you can export the preferences from that one
user/system, and then import them to the other side. When preferences are
exported, they are exported into an XML formatted document whose DTD is
specified by http://java.sun.com/dtd/preferences.dtd, though you don't
really need to know that. You can export either a whole subtree with
the exportSubtree() method or just a single node with the exportNode()
method. Both methods accept an OutputStream argument to specify where to
store things. The XML document will be UTF-8 character encoded. Importing
of the data then happens via the importPreferences() method, which takes
an InputStream argument. From an API perspective, there is no difference
in importing a system node/tree or a user node.
Adding a few lines of code to the previous example will export the newly
updated high score to the file high.xml. Much of the added code is
responsible for launching a new thread to save the file and for handling
exceptions. There are only three lines to export the single node:
Thread runner = new Thread(new Runnable() {
public void run() {
try {
FileOutputStream fis = new FileOutputStream("high.xml");
systemNode.exportNode(fis);
fis.close();
} catch (Exception e) {
Toolkit.getDefaultToolkit().beep();
Toolkit.getDefaultToolkit().beep();
Toolkit.getDefaultToolkit().beep();
}
}
});
runner.start();
When exported, the file will look something like the following:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE preferences SYSTEM
"http://java.sun.com/dtd/preferences.dtd">
<preferences EXTERNAL_XML_VERSION="1.0">
<root type="system">
<map/>
<node name="<unnamed>">
<map>
<entry key="High.highScore" value="95"/>
</map>
</node>
</root>
</preferences>
Notice the root element has a type attribute that says "system". This
states the type of node it is. The node also has a name attribute
valued at "<unnamed>". Since the High class was not placed in a package,
you get to work in the unnamed system node area. The entry attribute
provide the current high score value, 95 in the example here, though
your value could differ.
While we won't include any import code in the example here, the
way to import is just a static method call on Preferences, passing
in the appropriate input stream:
FileInputStream fis = new FileInputStream("high.xml");
Preferences.importPreferences(fis);
fis.close();
Since the XML file includes information about whether the preferences
are system or user type, the import call doesn't have to explicitly
include this bit of information. Besides the typical IOExceptions that
can happen, the import call will throw an InvalidPreferencesFormatException
if the file format is invalid. Exporting can also throw a
BackingStoreException if the data to export can't be read correctly
from the backing store.
Event Notifications
The original version of the High game updated the high score
preference, then explicitly made a call to update the label on the
screen. A better way to perform this action would be to add
a listener to the preferences node, then a value change can
automatically trigger the label to update its value. That way,
if the high score is ever updated from multiple places, you won't need
to remember to add code to update the label after saving the updated
value.
The two lines:
updateHighScore(next); updateHighScoreLabel(next);
can become one with the addition of the right listeners.
updateHighScore(next);
There is a PreferenceChangeListener and its associated PreferenceChangeEvent
for just such a task. The listener will be notified for all changes to the
associated node, so you need to check for which key-value pair was modified,
as shown here.
PreferenceChangeListener changeListener =
new PreferenceChangeListener() {
public void preferenceChange(PreferenceChangeEvent e) {
if (HIGH_SCORE.equals(e.getKey())) {
String newValue = e.getNewValue();
int value = Integer.valueOf(newValue);
updateHighScoreLabel(value);
}
}
};
systemNode.addPreferenceChangeListener(changeListener);
The PreferenceChangeEvent has three important properties: the key, new
new value, and the node itself. The new value doesn't have all the convenience
methods of Preferences though. For example, you can't retrieve the
value as an int. Instead you must manually convert the value
yourself. Here's what the modified High class looks like:
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.util.prefs.*;
import javax.swing.*;
public class High {
static JLabel highScore = new JLabel();
static JLabel score = new JLabel();
static Random random = new Random(new Date().getTime());
private static final String HIGH_SCORE = "High.highScore";
static Preferences systemNode =
Preferences.systemNodeForPackage(High.class);
public static void main (String args[]) {
/* -- Uncomment these lines to clear saved score
systemNode.remove(HIGH_SCORE);
*/
PreferenceChangeListener changeListener =
new PreferenceChangeListener() {
public void preferenceChange(PreferenceChangeEvent e) {
if (HIGH_SCORE.equals(e.getKey())) {
String newValue = e.getNewValue();
int value = Integer.valueOf(newValue);
updateHighScoreLabel(value);
}
}
};
systemNode.addPreferenceChangeListener(changeListener);
EventQueue.invokeLater(
new Runnable() {
public void run() {
JFrame frame = new JFrame("High Score");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
updateHighScoreLabel(getSavedHighScore());
frame.add(highScore, BorderLayout.NORTH);
frame.add(score, BorderLayout.CENTER);
JButton button = new JButton("Play");
ActionListener listener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
int next = random.nextInt(100);
score.setText(Integer.toString(next));
int old = getSavedHighScore();
if (next > old) {
Toolkit.getDefaultToolkit().beep();
updateHighScore(next);
}
}
};
button.addActionListener(listener);
frame.add(button, BorderLayout.SOUTH);
frame.setSize(200, 200);
frame.setVisible(true);
}
}
);
}
private static void updateHighScoreLabel(int value) {
if (value == -1) {
highScore.setText("");
} else {
highScore.setText(Integer.toString(value));
}
}
private static int getSavedHighScore() {
return systemNode.getInt(HIGH_SCORE, -1);
}
private static void updateHighScore(int value) {
systemNode.putInt(HIGH_SCORE, value);
// Save XML in separate thread
Thread runner = new Thread(new Runnable() {
public void run() {
try {
FileOutputStream fis = new FileOutputStream("high.xml");
systemNode.exportNode(fis);
fis.close();
} catch (Exception e) {
Toolkit.getDefaultToolkit().beep();
Toolkit.getDefaultToolkit().beep();
Toolkit.getDefaultToolkit().beep();
}
}
});
runner.start();
}
}
In addition to the PreferenceChangeListener/Event class pair, there is a
NodeChangeListener and NodeChangeEvent combo for notification of
preference changes. However, these are for notification nodes
additions and removals, not changing values of specific nodes. Of course, if you
are writing something like a Preferences viewer, clearly you'd want to know
if/when nodes appear and disappear so these classes may be of interest, too.
The whole Preferences API can be quite handy to store data beyond the life
of your application without having to rely on a database system. For more information
on the API, see the article Sir, What is Your Preference?