Running on Java 24-ea+24-2960 (Preview)
Home of The JavaSpecialists' Newsletter

030What Do You Prefer?

Author: Herman Lintvelt (Polymorph Systems)Date: 2001-09-17Java Version: 1.4Category: GUI
 

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.

What Do You Prefer?

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...

 

Comments

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)

When you load these comments, you'll be connected to Disqus. Privacy Statement.

Related Articles

Browse the Newsletter Archive

About the Author

Heinz Kabutz Java Conference Speaker

Java Champion, author of the Javaspecialists Newsletter, conference speaking regular... About Heinz

Superpack '23

Superpack '23 Our entire Java Specialists Training in one huge bundle more...

Free Java Book

Dynamic Proxies in Java Book
Java Training

We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.

Java Consulting

We can help make your Java application run faster and trouble-shoot concurrency and performance bugs...