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 MapsA 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 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:
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 programNOTE
NOTE
Example 10-14. CustomWorld.java1. 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
System InformationHere'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
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.java1. 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
The Preferences APIAs you have seen, the Properties class makes it simple to load and save configuration information. However, using property files has a number of disadvantages.
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.
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.java1. 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
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. |