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 classPreferencesEditor
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". 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 aPreferences
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 PreferencesEditorAnd 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...
created by Herman Lintvelt (Polymorph Systems)
0 komentar:
Posting Komentar