Abstract: We learn how to build a simple, yet usable, cross-platform registry editor using standard Java.
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@polymorph.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.
javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.
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".
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).
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 Preferences
object 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".
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...
We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)
We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.