The Java Specialists' Newsletter [Issue 030] - What do you Prefer?
Author: Herman Lintvelt (Polymorph Systems)
JDK version:
Category: GUI
You can subscribe from our home page:
http://www.javaspecialists.co.za (which also hosts all previous issues,
available free of charge
With the arrival of my second child, a 4.2kg daughter which we called Nicola
Constance Bettina Kabutz, I have been rather busy changing nappies, rocking the
child to sleep, and more exhaustingly, helping my 3 year old son cope with life
in general. Fortunately for my die-hard supporters out there, Herman Lintvelt (herman@javaspecialists.co.za)
stepped in and saved the day. I promise to pull my socks up and get these things
done more regularly as soon as life returns to stability.
With regards
Heinz
Recently I've downloaded JDK 1.4 beta 2, and then forgot about it for a while
as my struggles with JMF required all my resources. But then, on a cold
Worcester (South Africa) evening, while sitting in front of my fireplace with a
nice warm fire heating the room, I was thinking about things I prefer. My
preferences.
Do not worry, I won't carry on being philosophical. It actually reminded me
of the new Preferences API in JDK1.4, so I put away the red wine, and pulled out
my laptop. And was I pleasantly surprised.
The Preferences API
The guys at Sun has seen the need for handling preferences in a somewhat
better and easier to use way than using Properties or implementing
a complex preference subsystem that saves it to a database or some other backing
store. So they created the Preferences API.
Using this API starts with the Preferences class in the
java.util.prefs package. This class represents a preference node in some
kind of preference hierarchy. Such a node can have child nodes, as well as
key-value pairs belonging to it (similar to Windows Registry). The 4 important
static methods of Preferences are:
// return system preference-node for package that O belongs to
Preferences systemNodeForPackage(Object 0);
// return root node of system preferences
Preferences systemRoot();
// return system preference-node for package that O belongs to
Preferences userNodeForPackage(Object O);
// return root node of user preferences
Preferences userRoot();
Some explanation is probably needed. The preference data gets saved in two
tree-like structures in an implementation specific way. The JDK for Windows
version saves it in the Windows Registry, but it is possible to create one's own
implementation that might for example use a database. The one tree is used to
store user-specific preferences (each user on a system will have a seperate
tree), and the other tree stores system preferences. (The definition of user and
system depends on the preferences implementation. In the Windows JDK version it
maps to Windows users and system-wide preferences.) Each node in this tree can
be represented by a Preferences object.
However, if you're like me you do not like theory too much (and that's what
javadocs are for), so let us explore this API with an example: a "Cross-platform
Registry Editor".
Cross-platform Registry Editor
The idea of this Java tool is to be able to view and edit preferences saved
via the Preferences API, no matter on what platform it is executed (i.e. the
backing store used is transparent to the user).
Preference Nodes
We implement the class PreferencesEditor as a JDialog, and it
must contain a JTree to present the preferences trees (user and/or system), and
a JTable to display and edit the actual preference values. We need the following
inner classes: PrefTreeNode to represent a preference node in the
JTree, PrefTableModel to handle the display and editing of
preference values in the table, and PrefTreeSelectionListener to
update the JTable with the currently selected preference node.
I list the code for PreferenceEditor with discussions in between
the code.
//add all other necessary imports here
import java.util.prefs.Preferences;
import java.util.prefs.BackingStoreException;
public class PreferencesEditor extends JDialog {
JTree prefTree;
JTable editTable;
/**
* Creates PreferencesEditor dialog that show all System and
* User preferences.
* @param owner owner JFrame
* @param title title of dialog
*/
public PreferencesEditor(JFrame owner, String title){
this(owner, title, null, true, null, true);
}
/**
* @param owner owner JFrame
* @param title title of dialog
* @param userObj the package to which this object belongs is
* used as the root-node of the User preferences tree (if
* userObj is null, then the rootnode of all user preferences
* will be used)
* @boolean showUserPrefs if true, then show user preferences
* @param systemObj the package to which this object belongs is
* used as the root-node of the System preferences tree (if
* systemObj is null, then the rootnode of all system
* preferences will be used)
* @param showSystemPrefs if true, then show system preferences
*/
public PreferencesEditor(JFrame owner, String title,
Object userObj, boolean showUserPrefs, Object systemObj,
boolean showSystemPrefs) {
super(owner, title);
getContentPane().setLayout(new BorderLayout(5,5));
setSize(640,480);
createTree(userObj, showUserPrefs, systemObj, showSystemPrefs);
editTable = new JTable();
createSplitPane();
createButtonPanel();
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
As mentioned in the code comments, there are two constructors: one give
access to all the system and user preferences, and one can be used to only
display/edit a specified subset of the preferences. Let's first look at the
createTree(...), createUserNode(...) and
createSystemRootNode(...) methods to see how this is done:
private void createTree(Object userObj, boolean showUserPrefs, Object systemObj, boolean showSystemPrefs){
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode("Preferences");
if (showUserPrefs) {
rootNode.add(createUserRootNode(userObj));
}
if (showSystemPrefs) {
rootNode.add(createSystemRootNode(systemObj));
}
DefaultTreeModel model = new DefaultTreeModel(rootNode);
prefTree = new JTree(model);
prefTree.addTreeSelectionListener(new PrefTreeSelectionListener());
}
private MutableTreeNode createSystemRootNode(Object obj) {
try {
PrefTreeNode systemRoot;
if (obj==null) {
systemRoot = new PrefTreeNode(Preferences.systemRoot());
} else {
systemRoot = new PrefTreeNode(Preferences.systemNodeForPackage(obj));
}
return systemRoot;
} catch (BackingStoreException e) {
e.printStackTrace();
return new DefaultMutableTreeNode("No System Preferences!");
}
}
private MutableTreeNode createUserRootNode(Object obj) {
try {
PrefTreeNode userRoot;
if (obj==null) {
userRoot = new PrefTreeNode(Preferences.userRoot());
} else {
userRoot = new PrefTreeNode(Preferences.userNodeForPackage(obj));
}
return userRoot;
} catch (BackingStoreException e) {
e.printStackTrace();
return new DefaultMutableTreeNode("No User Preferences!");
}
}
If the user specify a userObj (and showUserPrefs=true),
then Preferences.userNodeForPackage(userObj) gets called in
creteUserRootNode. This will return a Preferences object
that represents the preferences node that maps to the package structure of
userObj. If this preference node does not yet exist in the backing store,
it gets created. For example, if I call createUserNode(new
com.polymorph.MyClass()), then the preference node "com/polymorph" will
be returned, and its parent node will be "com" (in the user preference tree). If
the user pass null as
parameter, then Preferences.userRoot() gets called, which return
the root node of the user preferences tree (for the current user). The same goes
for createSystemRootNode and the system preferences.
Of course we need a way of representing a preference node in a JTree, and
this is what the PrefTreeNode inner class is for.
class PrefTreeNode extends DefaultMutableTreeNode {
Preferences pref;
String nodeName;
String[] childrenNames;
public PrefTreeNode(Preferences pref) throws BackingStoreException {
this.pref = pref;
childrenNames = pref.childrenNames();
}
public Preferences getPrefObject(){
return pref;
}
public boolean isLeaf(){
return ((childrenNames==null)||(childrenNames.length == 0));
}
public int getChildCount(){
return childrenNames.length;
}
public TreeNode getChildAt(int childIndex){
if(childIndex < childrenNames.length){
try {
PrefTreeNode child = new PrefTreeNode(pref.node(childrenNames[childIndex]));
return child;
} catch (BackingStoreException e) {
e.printStackTrace();
return new DefaultMutableTreeNode("Problem Child!");
}
}
return null;
}
public String toString(){
String name = pref.name();
if ((name == null)||("".equals(name))){ //if root node
name = "System Preferences";
if (pref.isUserNode()) name = "User Preferences";
}
return name;
}
}
This inner class decorates a Preferences object to be used as a
MutableTreeNode in a JTree. All the child preferences nodes of this
object are accessed via the pref.childrenNames() call, and stored
in a String array. This array is then used to calculate the number of children
nodes, whether this is a leaf node, etc. getChildAt gets a specific
child node, mainly via the pref.node(childrenNames[childIndex])
call. Preferences.node("nodeString")
returns a Preferencesobject for the node specified by "nodeString".
It can specify a node relative to the current one, ex. "child1", which will
return a child preference node (as we use it in getChildAt), or an
absolute path can be specified, ex. "/com/polymorph/UI" will return a node "UI",
with parent node "polymorph".
Preference Key-Value pairs
OK, our editor is now able to handle the nodes in the user and/or system
preferences hierarchy, but how to we actually access the preference values?
Well, the Preferences API allows us to save preferences in our custom defined
preferences structure in a very similar way as we would in a Hashmap: we put
key-value pairs in the preferences node, where the key is a specific preference
setting name, and the value can either be a String, int, long, boolean, float,
double or byte[]. Once you have a Preferences object, you can just
call put("keyStr",
"valueStr"), or putLong("keyStr",
123l), etc. And you can retrieve
these values via the get("keyStr",
"defaultStr"), or getLong("keyStr",
233 /*defaultVal*/),
etc. methods. Note that for every get method, a default value must be supplied.
This forces you to think about default values for when the preferences cannot be
loaded from the backing store, thus allowing your application to continue even
though preferences could not be loaded.
In our editor example, we access these key-value pairs in a JTable, and we
need the PrefTableModel to do this:
class PrefTableModel extends AbstractTableModel {
Preferences pref;
String[] keys;
public PrefTableModel(Preferences pref){
this.pref = pref;
try {
keys = pref.keys();
} catch (BackingStoreException e) {
System.out.println("Could not get keys for Preference node: "+pref.name());
e.printStackTrace();
keys = new String[0];
}
}
public String getColumnName(int column) {
switch(column){
case 0: return "Key";
case 1: return "Value";
default: return "-";
}
}
public boolean isCellEditable(int rowIndex, int columnIndex) {
switch(columnIndex) {
case 0: return false;
case 1: return true;
default: return false;
}
}
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
pref.put(keys[rowIndex], aValue.toString());
try {
pref.sync(); //make sure the backing store is synchronized with latest update
} catch (BackingStoreException e) {
System.out.println("Error synchronizing backStore with updated value");
e.printStackTrace();
}
}
public Object getValueAt(int row, int column){
String key = keys[row];
if (column==0) return key;
Object value = pref.get(key, "(Unknown)");
return value;
}
public int getColumnCount(){
return 2;
}
public int getRowCount(){
return keys.length;
}
}
In the PrefTableModel constructor, pref.keys returns
all the key-names stored in the pref node. These key-names are then
used in getValueAt to get the value of either a key or value-column
cell. If we want the value of a Value-column cell, we get it as a String
via the pref.get(key, "Unknown")
call (default value="Unknown"), as the Preferences API unfortunately does not
seem to allow us to retrieve it as an Object. Thus all values are
presented as String in the table, but this should not be a problem, as it seems
that these values are saved as Strings anyway in the backing store.
getLong, getBoolean, etc. tries and interpret the saved string-value as a
long, boolean,
etc.
Only the Value-column cells are editable, and the setValueAt
method uses pref.put(key-name, aValue) to update the edited value.
It also calls pref.sync() that forces any updates to be
synchronized with the backing store.
How do we connect this table model to the preference tree? Well, the
PreferencesEditor constructor creates a JTable object (editTable),
and then we use the PrefTreeSelectionListener inner class to update
the table model of this table.
class PrefTreeSelectionListener implements TreeSelectionListener{
public void valueChanged(TreeSelectionEvent e) {
try {
PrefTreeNode node = (PrefTreeNode)e.getPath().getLastPathComponent();
Preferences pref = node.getPrefObject();
editTable.setModel(new PrefTableModel(pref));
} catch (ClassCastException ce) {
System.out.println("Node not PrefTreeNode!");
editTable.setModel(new DefaultTableModel());
}
}
}
The createTree method adds an instance of
PrefTreeSelectionListener to the JTree as a listener.
All that now remains to be defined are the createSplitPane() and
createButtonPanel methods, and none of them contains any surprises:
private void createSplitPane(){
JSplitPane splitPane = new JSplitPane();
splitPane.setOrientation(JSplitPane.HORIZONTAL_SPLIT);
splitPane.setOneTouchExpandable(true);
splitPane.setLeftComponent(new JScrollPane(prefTree));
splitPane.setRightComponent(new JScrollPane(editTable));
getContentPane().add(splitPane, BorderLayout.CENTER);
}
private void createButtonPanel(){
JPanel buttonPanel = new JPanel(new BorderLayout(5,5));
JButton closeButton = new JButton("Close");
closeButton.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e) {
System.exit(0);
}
});
buttonPanel.add(closeButton, BorderLayout.EAST);
getContentPane().add(buttonPanel, BorderLayout.SOUTH);
}
} //end of PreferencesEditor
And that's how easy it is to implement a simple, yet usable, cross-platform
registry editor. Already my mind is spinning with ideas on how to improve on
this, like adding functionality to be able to modify the preference trees and
making use of the Preferences API export/import capabilities (yhep, you can
actually export preferences to XML files, and also import these files). A whole
new preferable world is opening up...
Copyright 2000-2004 Maximum Solutions, South Africa
Reprint Rights. Copyright subsists in all the material included
in this email, but you may freely share the entire email with anyone you feel
may be interested, and you may reprint excerpts both online and offline provided
that you acknowledge the source as follows: This material from The Java(tm)
Specialists' Newsletter by Maximum Solutions (South Africa). Please contact
Maximum Solutions for more
information.
Java and Sun are trademarks or registered trademarks of Sun Microsystems,
Inc. in the United States and other countries. Maximum Solutions is independent
of Sun Microsystems, Inc.
21032 bytes more | comments? | | Score: 0
|