Storage of Application Preferences

   


Most programs can be configured by their users. The programs must then save the user preferences and restore them when the application starts again. You already saw how a Java Web Start application can use a persistent store for that purpose. However, JDK 1.4 introduced a different and more powerful mechanism for local applications. We describe that mechanism, but first we cover the much simpler approach for storing configuration information that Java applications have traditionally taken.

Property Maps

A property map is a data structure that stores key/value pairs. Property maps are often used for storing configuration information. Property maps have three particular characteristics:

  • The keys and values are strings.

  • The set can easily be saved to a file and loaded from a file.

  • There is a secondary table for default values.

The Java platform class that implements a property map is called Properties.

Property maps are useful in specifying configuration options for programs. For example,

 Properties settings = new Properties(); settings.put("font", "Courier"); settings.put("size", "10"); settings.put("message", "Hello, World"); 

Use the store method to save this list of properties to a file. Here, we just save the property map in the file Myprog.properties. The second argument is a comment that is included in the file.

 FileOutputStream out = new FileOutputStream("Myprog.properties"); settings.store(out, "Myprog Properties"); 

The sample set gives the following output.

 #Myprog Properties #Tue Jun 15 07:22:52  2004 font=Courier size=10 message=Hello, World 

To load the properties from a file, use

 FileInputStream in = new FileInputStream("Myprog.properties"); settings.load(in); 

We'll put this technique to work so that your users can customize the NotHelloWorld program to their hearts' content. We'll allow them to specify the following in the configuration file CustomWorld.properties:

  • Window size

  • Font

  • Point size

  • Text color

  • Message string

If the user doesn't specify some of the settings, we will provide defaults.

The Properties class has two mechanisms for providing defaults. First, whenever you look up the value of a string, you can specify a default that should be used automatically when the key is not present.

 String font = settings.getProperty("font", "Courier"); 

If there is a "font" property in the property table, then font is set to that string. Otherwise, font is set to "Courier".

If you find it too tedious to specify the default in every call to getProperty, then you can pack all the defaults into a secondary property map and supply that map in the constructor of your lookup table.

 Properties defaultSettings = new Properties(); defaultSettings.put("font", "Courier"); defaultSettings.put("size", "10"); defaultSettings.put("color.red", "0"); . . . Properties settings = new Properties(defaultSettings); FileInputStream in = new FileInputStream("CustomWorld.properties"); settings.load(in); . . . 

Yes, you can even specify defaults to defaults if you give another property map parameter to the defaultSettings constructor, but it is not something one would normally do.

Example 10-14 is the customizable "Hello, Custom World" program. Just edit the .properties file to change the program's appearance to the way you want (see Figure 10-17).

Figure 10-17. The customized Hello, World program


NOTE

Properties are simple tables without a hierarchical structure. It is common to introduce a fake hierarchy with key names such as window.main.color, window.main.title, and so on. But the Properties class has no methods that help organize such a hierarchy. If you store complex configuration information, you should use the Preferences class instead see the next section.


NOTE

The Properties class extends the Hashtable class. That means that all Hashtable methods are available to Properties objects. Some methods are useful. For example, size returns the number of possible properties (well, it isn't that nice it doesn't count the defaults). Similarly, keys returns an enumeration of all keys, except for the defaults. There is also a second method, called propertyNames, that returns all keys. The put method is downright dangerous. It doesn't check that you put strings into the table.

Does the is-a rule for using inheritance apply here? Is every property map a hash table? Not really. That these are true is really just an implementation detail. Maybe it is better to think of a property map as having a hash table. But then the hash table should be a private instance variable. Actually, in this case, a property map uses two hash tables: one for the defaults and one for the nondefault values.

We think a better design would be the following:

 class Properties {    public String getProperty(String) { . . . }    public void put(String, String) { . . . }    . . .    private Hashtable nonDefaults;    private Hashtable defaults; } 

We don't want to tell you to avoid the Properties class in the Java library. Provided you are careful to put nothing but strings in it, it works just fine. But think twice before using "quick and dirty" inheritance in your own programs.


Example 10-14. CustomWorld.java
  1. import java.awt.*;  2. import java.awt.event.*;  3. import java.util.*;  4. import java.io.*;  5. import javax.swing.*;  6.  7. /**  8.    This program demonstrates how to customize a "Hello, World"  9.    program with a properties file. 10. */ 11. public class CustomWorld 12. { 13.    public static void main(String[] args) 14.    { 15.       CustomWorldFrame frame = new CustomWorldFrame(); 16.       frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 17.       frame.setVisible(true); 18.    } 19. } 20. 21. /** 22.    This frame displays a message. The frame size, message text, 23.    font, and color are set in a properties file. 24. */ 25. class CustomWorldFrame extends JFrame 26. { 27.    public CustomWorldFrame() 28.    { 29.       Properties defaultSettings = new Properties(); 30.       defaultSettings.put("font", "Monospaced"); 31.       defaultSettings.put("width", "300"); 32.       defaultSettings.put("height", "200"); 33.       defaultSettings.put("message", "Hello, World"); 34.       defaultSettings.put("color.red", "0 50 50"); 35.       defaultSettings.put("color.green", "50"); 36.       defaultSettings.put("color.blue", "50"); 37.       defaultSettings.put("ptsize", "12"); 38. 39.       Properties settings = new Properties(defaultSettings); 40.       try 41.       { 42.          FileInputStream in = new FileInputStream("CustomWorld.properties"); 43.          settings.load(in); 44.       } 45.       catch (IOException e) 46.       { 47.          e.printStackTrace(); 48.       } 49. 50.       int red = Integer.parseInt(settings.getProperty("color.red")); 51.       int green = Integer.parseInt(settings.getProperty("color.green")); 52.       int blue = Integer.parseInt(settings.getProperty("color.blue")); 53. 54.       Color foreground = new Color(red, green, blue); 55. 56.       String name = settings.getProperty("font"); 57.       int ptsize = Integer.parseInt(settings.getProperty("ptsize")); 58.       Font f = new Font(name, Font.BOLD, ptsize); 59. 60.       int hsize = Integer.parseInt(settings.getProperty("width")); 61.       int vsize = Integer.parseInt(settings.getProperty("height")); 62.       setSize(hsize, vsize); 63.       setTitle(settings.getProperty("message")); 64. 65.       JLabel label = new JLabel(settings.getProperty("message"), SwingConstants.CENTER); 66.       label.setFont(f); 67.       label.setForeground(foreground); 68.       add(label); 69.    } 70. } 


 java.util.Properties 1.0 

  • Properties()

    creates an empty property list.

  • Properties(Properties defaults)

    creates an empty property list with a set of defaults.

    Parameters:

    defaults

    The defaults to use for lookups


  • String getProperty(String key)

    gets a property association. Returns the string associated with the key, or the string associated with the key in the default table if it wasn't present in the table, or null if the key wasn't present in the default table either.

    Parameters:

    key

    The key whose associated string to get


  • String getProperty(String key, String defaultValue)

    gets a property with a default value if the key is not found. Returns the string associated with the key, or the default string if it wasn't present in the table.

    Parameters:

    key

    The key whose associated string to get

    defaultValue

    The string to return if the key is not present


  • void load(InputStream in) throws IOException

    loads a property map from an input stream.

    Parameters:

    in

    The input stream


  • void store(OutputStream out, String header) 1.2

    saves a property map to an output stream.

    Parameters:

    out

    The output stream

    header

    The header in the first line of the stored file


System Information

Here's another example of the ubiquity of the Properties set. Information about your system is stored in a Properties object that is returned by the static getProperties method of the System class:

 Properties sysprops = System.getProperties(); 

To access a single property, call

 String value = System.getProperty(key); 

Applications that run without a security manager have complete access to this information, but applets and other untrusted programs can access only the following keys:

 java.version java.vendor java.vendor.url java.class.version os.name os.version os.arch file.separator path.separator line.separator java.specification.version java.vm.specification.version java.vm.specification.vendor java.vm.specification.name java.vm.version java.vm.vendor java.vm.name 

If an applet attempts to read another key (or all properties), then a security exception is thrown.

NOTE

You can find the names of the freely accessible system properties in the file security/java.policy in the directory of the Java run time.


The program in Example 10-15 prints out the key/value pairs in the Properties object that stores the system properties.

Here is an example of what you would see when you run the program. You can see all the values stored in this Properties object. (What you would get will, of course, reflect your machine's settings.)

#System Properties #Sat May 08 08:27:34 PDT 2004 java.runtime.name=Java(TM) 2 Runtime Environment, Standard Edition sun.boot.library.path=/usr/local/jdk5.0/jre/lib/i386 java.vm.version=5.0 java.vm.vendor=Sun Microsystems Inc. java.vendor.url=http\://java.sun.com/ path.separator=\: java.vm.name=Java HotSpot(TM) Client VM file.encoding.pkg=sun.io user.country=US sun.os.patch.level=unknown java.vm.specification.name=Java Virtual Machine Specification user.dir=/home/cay/books/cj5/corejava/v1/v1ch10/SystemInfo java.runtime.version=5.0 java.awt.graphicsenv=sun.awt.X11GraphicsEnvironment java.endorsed.dirs=/usr/local/jdk5.0/jre/lib/endorsed os.arch=i386 java.io.tmpdir=/tmp line.separator=\n java.vm.specification.vendor=Sun Microsystems Inc. os.name=Linux java.library.path=/usr/local/jdk5.0/jre/lib/i386/client\:/usr/local/jdk5.0/jre/lib/i386\: /usr/local/jdk5.0/jre/../lib/i386 java.specification.name=Java Platform API Specification java.class.version=48.0 sun.management.compiler=HotSpot Client Compiler java.util.prefs.PreferencesFactory=java.util.prefs.FileSystemPreferencesFactory os.version=2.4.20-8 user.home=/home/cay user.timezone=America/Los_Angeles java.awt.printerjob=sun.print.PSPrinterJob file.encoding=UTF-8 java.specification.version=5.0 java.class.path=. user.name=cay java.vm.specification.version=1.0 java.home=/usr/local/jdk5.0/jre sun.arch.data.model=32 user.language=en java.specification.vendor=Sun Microsystems Inc. java.vm.info=mixed mode java.version=5.0 java.ext.dirs=/usr/local/jdk5.0/jre/lib/ext sun.boot.class.path=/usr/local/jdk5.0/jre/lib/rt.jar\:/usr/local/jdk5.0/jre/lib/i18n.jar\: /usr/local/jdk5.0/jre/lib/sunrsasign.jar\:/usr/local/jdk5.0/jre/lib/jsse.jar\:/usr/local /jdk5.0/jre/lib/jce.jar\:/usr/local/jdk5.0/jre/lib/charsets.jar\:/usr/local/jdk5.0/jre/classes java.vendor=Sun Microsystems Inc. file.separator=/ java.vendor.url.bug=http\://java.sun.com/cgi-bin/bugreport.cgi sun.io.unicode.encoding=UnicodeLittle sun.cpu.endian=little sun.cpu.isalist=

Example 10-15. SystemInfo.java
  1. import java.applet.*;  2. import java.io.*;  3. import java.util.*;  4  5. /**  6.    This program prints out all system properties.  7. */  8. public class SystemInfo  9. { 10.    public static void main(String args[]) 11.    { 12.       try 13.       { 14.          Properties sysprops = System.getProperties(); 15.          sysprops.store(System.out, "System Properties"); 16.       } 17.       catch (IOException e) 18.       { 19.          e.printStackTrace(); 20.       } 21.    } 22. } 


 java.lang.System 1.0 

  • Properties getProperties()

    retrieves all system properties. The application must have permission to retrieve all properties or a security exception is thrown.

  • String getProperty(String key)

    retrieves the system property with the given key name. The application must have permission to retrieve the property or a security exception is thrown.

The Preferences API

As you have seen, the Properties class makes it simple to load and save configuration information. However, using property files has a number of disadvantages.

  • The configuration files cannot always be stored in the same location as the program because that location may not be writable. For example, it may be a read-only directory or a JAR file.

  • Multiple users may want to configure the same application in different ways.

  • Configuration files cannot always be stored in the user's home directory. Some operating systems (such as Windows 9x) have no concept of a home directory.

  • There is no standard convention for naming configuration files, increasing the likelihood of name clashes as users install multiple Java applications.

Some operating systems have a central repository for configuration information. The best-known example is the registry in Microsoft Windows. The Preferences class of JDK 1.4 provides such a central repository in a platform-independent manner. In Windows, the Preferences class uses the registry for storage; on Linux, the information is stored in the local file system instead. Of course, the repository implementation is transparent to the programmer using the Preferences class.

The Preferences repository has a tree structure, with node path names such as /com/mycompany/myapp. As with package names, name clashes are avoided as long as programmers start the paths with reversed domain names. In fact, the designers of the API suggest that the configuration node paths match the package names in your program.

Each node in the repository has a separate table of key/value pairs that you can use to store numbers, strings, or byte arrays. No provision is made for storing serializable objects. The API designers felt that the serialization format is too fragile for long-term storage. Of course, if you disagree, you can save serialized objects in byte arrays.

For additional flexibility, there are multiple parallel trees. Each program user has one tree, and an additional tree, called the system tree, is available for settings that are common to all users. The Preferences class uses the operating system notion of the "current user" for accessing the appropriate user tree.

To access a node in the tree, start with the user or system root:

 Preferences root = Preferences.userRoot(); 

or

 Preferences root = Preferences.systemRoot(); 

Then access the node. You can simply provide a node path name:

 Preferences node = root.node("/com/mycompany/myapp"); 

A convenient shortcut gets a node whose path name equals the package name of a class. Simply take an object of that class and call

 Preferences node = Preferences.userNodeForPackage(obj.getClass()); 

or

 Preferences node = Preferences.systemNodeForPackage(obj.getClass()); 

Typically, obj will be the this reference.

Once you have a node, you can access the key/value table with methods

 String get(String key, String defval) int getInt(String key, int defval) long getLong(String key, long defval) float getFloat(String key, float defval) double getDouble(String key, double defval) boolean getBoolean(String key, boolean defval) byte[] getByteArray(String key, byte[] defval) 

Note that you must specify a default value when reading the information, in case the repository data is not available. Defaults are required for several reasons. The data might be missing because the user never specified a preference. Certain resource-constrained platforms might not have a repository, and mobile devices might be temporarily disconnected from the repository.

Conversely, you can write data to the repository with put methods such as

 put(String key, String value) putInt(String key, int value) 

and so on.

You can enumerate all keys stored in a node with the method

 String[] keys 

But there is currently no way to find out the type of the value of a particular key.

Central repositories such as the Windows registry traditionally suffer from two problems.

  • They turn into a "dumping ground," filled with obsolete information.

  • Configuration data gets entangled into the repository, making it difficult to move preferences to a new platform.

The Preferences class has a solution for the second problem. You can export the preferences of a subtree (or, less commonly, a single node) by calling the methods

 void exportSubtree(OutputStream out) void exportNode(OutputStream out) 

The data are saved in XML format. You can import them into another repository by calling

 void importPreferences(InputStream in) 

Here is a sample file:

 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE preferences SYSTEM "http://java.sun.com/dtd/preferences.dtd"> <preferences EXTERNAL_XML_VERSION="1.0">   <root type="user">     <map/>     <node name="com">       <map/>       <node name="horstmann">         <map/>         <node name="corejava">           <map>             <entry key="left" value="11"/>             <entry key="top" value="9"/>             <entry key="width" value="453"/>             <entry key="height" value="365"/>           </map>         </node>       </node>     </node>   </root> </preferences> 

If your program uses preferences, you should give your users the opportunity of exporting and importing them, so they can easily migrate their settings from one computer to another. The program in Example 10-16 demonstrates this technique. The program simply saves the position and size of the main window. Try resizing the window, then exit and restart the application. The window will be just like you left it when you exited.

Example 10-16. PreferencesTest.java

   1. import java.awt.*;   2. import java.awt.event.*;   3. import java.io.*;   4. import java.util.logging.*;   5. import java.util.prefs.*;   6. import javax.swing.*;   7. import javax.swing.event.*;   8.   9. /**  10.    A program to test preference settings. The program  11.    remembers the frame position and size.  12. */  13. public class PreferencesTest  14. {  15.    public static void main(String[] args)  16.    {  17.       PreferencesFrame frame = new PreferencesFrame();  18.       frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);  19.       frame.setVisible(true);  20.    }  21. }  22.  23. /**  24.    A frame that restores position and size from user  25.    preferences and updates the preferences upon exit.  26. */  27. class PreferencesFrame extends JFrame  28. {  29.    public PreferencesFrame()  30.    {  31.       setTitle("PreferencesTest");  32.  33.       // get position, size from preferences  34.  35.       Preferences root = Preferences.userRoot();  36.       final Preferences node = root.node("/com/horstmann/corejava");  37.       int left = node.getInt("left", 0);  38.       int top = node.getInt("top", 0);  39.       int width = node.getInt("width", DEFAULT_WIDTH);  40.       int height = node.getInt("height", DEFAULT_HEIGHT);  41.       setBounds(left, top, width, height);  42.  43.       // set up file chooser that shows XML files  44.  45.       final JFileChooser chooser = new JFileChooser();  46.       chooser.setCurrentDirectory(new File("."));  47.  48.       // accept all files ending with .xml  49.       chooser.setFileFilter(new  50.          javax.swing.filechooser.FileFilter()  51.          {  52.             public boolean accept(File f)  53.             {  54.                return f.getName().toLowerCase().endsWith(".xml") || f.isDirectory();  55.             }  56.             public String getDescription()  57.             {  58.                return "XML files";  59.             }  60.          });  61.  62.  63.       // set up menus  64.       JMenuBar menuBar = new JMenuBar();  65.       setJMenuBar(menuBar);  66.       JMenu menu = new JMenu("File");  67.       menuBar.add(menu);  68.  69.       JMenuItem exportItem = new JMenuItem("Export preferences");  70.       menu.add(exportItem);  71.       exportItem.addActionListener(new  72.          ActionListener()  73.          {  74.             public void actionPerformed(ActionEvent event)  75.             {  76.                if(chooser.showSaveDialog(PreferencesFrame.this) == JFileChooser .APPROVE_OPTION)  77.                {  78.                   try  79.                   {  80.                      OutputStream out = new FileOutputStream(chooser.getSelectedFile());  81.                      node.exportSubtree(out);  82.                      out.close();  83.                   }  84.                   catch (Exception e)  85.                   {  86.                      e.printStackTrace();  87.                   }  88.                }  89.             }  90.          });  91.  92.       JMenuItem importItem = new JMenuItem("Import preferences");  93.       menu.add(importItem);  94.       importItem.addActionListener(new  95.          ActionListener()  96.          {  97.             public void actionPerformed(ActionEvent event)  98.             {  99.                if(chooser.showOpenDialog(PreferencesFrame.this) == JFileChooser .APPROVE_OPTION) 100.                { 101.                   try 102.                   { 103.                      InputStream in = new FileInputStream(chooser.getSelectedFile()); 104.                      node.importPreferences(in); 105.                      in.close(); 106.                   } 107.                   catch (Exception e) 108.                  { 109.                     e.printStackTrace(); 110.                  } 111.               } 112.            } 113.         }); 114. 115.       JMenuItem exitItem = new JMenuItem("Exit"); 116.       menu.add(exitItem); 117.       exitItem.addActionListener(new 118.          ActionListener() 119.          { 120.             public void actionPerformed(ActionEvent event) 121.             { 122.                node.putInt("left", getX()); 123.                node.putInt("top", getY()); 124.                node.putInt("width", getWidth()); 125.                node.putInt("height", getHeight()); 126.                System.exit(0); 127.             } 128.          }); 129.    } 130.    public static final int DEFAULT_WIDTH = 300; 131.    public static final int DEFAULT_HEIGHT = 200; 132. } 


 java.util.prefs.Preferences 1.4 

  • Preferences userRoot()

    returns the root preferences node of the user of the calling program.

  • Preferences systemRoot()

    returns the systemwide root preferences node.

  • Preferences node(String path)

    returns a node that can be reached from the current node by the given path. If path is absolute (that is, starts with a /), then the node is located starting from the root of the tree containing this preference node. If there isn't a node with the given path, it is created.

  • Preferences userNodeForPackage(Class cl)

  • Preferences systemNodeForPackage(Class cl)

    return a node in the current user's tree or the system tree whose absolute node path corresponds to the package name of the class cl.

  • String[] keys()

    returns all keys belonging to this node.

  • String get(String key, String defval)

  • int getInt(String key, int defval)

  • long getLong(String key, long defval)

  • float getFloat(String key, float defval)

  • double getDouble(String key, double defval)

  • boolean getBoolean(String key, boolean defval)

  • byte[] getByteArray(String key, byte[] defval)

    return the value associated with the given key, or the supplied default value if no value is associated with the key, or the associated value is not of the correct type, or the preferences store is unavailable.

  • void put(String key, String value)

  • void putInt(String key, int value)

  • void putLong(String key, long value)

  • void putFloat(String key, float value)

  • void putDouble(String key, double value)

  • void putBoolean(String key, boolean value)

  • void putByteArray(String key, byte[] value)

    store a key/value pair with this node.

  • void exportSubtree(OutputStream out)

    writes the preferences of this node and its children to the specified stream.

  • void exportNode(OutputStream out)

    writes the preferences of this node (but not its children) to the specified stream.

  • void importPreferences(InputStream in)

    imports the preferences contained in the specified stream.

This concludes our discussion of Java software deployment. In the next chapter, you learn how to use exceptions to tell your programs what to do when problems arise at run time. We also give you tips and techniques for testing and debugging so that not too many things will go wrong when your programs run.


       
    top



    Core Java 2 Volume I - Fundamentals
    Core Java(TM) 2, Volume I--Fundamentals (7th Edition) (Core Series) (Core Series)
    ISBN: 0131482025
    EAN: 2147483647
    Year: 2003
    Pages: 132

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net