Java Periphery


Java gives you a number of mechanisms to help your application interact with the local operating environment.

Properties

A small hash table of system properties is always available from any point in your application. This table contains useful information, including things about your Java environment and your operating system. You can obtain the entire set of properties using the java.lang.System method getProperties:

 Properties existingProperties = System.getProperties(); 

The java.util.Properties class extends the old java.util.Hashtable class. It adds a number of management methods and utility methods. The management methods help you do things such as load a set of properties from a file. The utility methods simplify access to the hash table.

The API documentation for the java.lang.System class method getProperties shows the list of predefined properties. Some of the more useful properties include:

Java.class.path

value of the Java classpath[12]

Java.io.tmpdir

the default temp file path

file.separator

the file separator ('/' under Unix; '\' under Windows)

path.separator

the separator between entries in a path (':' under Unix; ';' under Windows)

line.separator

the line separator ('\n' under Unix; '\r\n' under Windows)

user.home

the current user's home directory

user.dir

the current working directory


[12] You can set this property to a new value, but it has no effect on the current Java execution. The Java VM does not reread this value. If you think you need to change the classpath, you may need to build a custom class loader instead.

Using these properties, you can code your application to execute on different operating systems without change.

You retrieve the value of a single system property using the System class method getProperty:

 assertEquals("1.5.0-beta3", System.getProperty("java.version")); 

System properties are global. You can access them from anywhere in your code.

You can add your own global properties to the list of system properties. Generally, a database is a better place for this sort of information. Your main reason to use properties should be to dynamically alter small pieces of information from one application execution to the next. For example, your deployed application may need to execute in "kiosk" mode most of the time, looping and occupying the full screen without borders or a title bar. During development, you may want to be able to switch rapidly back and forth between execution modes.

You can accomplish this dynamism in a number of ways. You could put a mode setting flag into a database, but having to modify data in a database can slow you down. You could pass the information as command line arguments and manage it in your main method. But you might not need the information until later in your application; what do you do with it until then?

You can add your property to the system properties programmatically:

 System.setProperty("kioskMode", "loopFullscreen"); 

Or, as you saw with logging in Lesson 8, you can set the property as a VM argument:

 java DkioskMode=noloopFullscreen sis.Sis 

If a property is not set, getProperty returns null. You can provide a default value to getProperty to be returned if the property is not set. A language test shows how this works:

 public void testGetPropertyWithDefault() {    Properties existingProperties = System.getProperties();    try {       System.setProperties(new Properties()); // removes all properties       assertEquals(          "default", System.getProperty("noSuchProperty", "default"));    }    finally {       System.setProperties(existingProperties);    } } 

Property Files

One technique you will encounter in many shops and many applications is the use of property files. A property file is similar to a resource file; it is a collection of key-value pairs in the form key=value. Developers often use property files as a quick-and-dirty catchall for configurable values. Things such as class names of drivers, color schemes, and boolean flags end up in property files.

One driving force is the need to change certain values as software moves from a production environment into a test environment, an integration environment, or a production environment. For example, your code might need to connect to a web server. The web server name you use for development will differ from the web server name for production.

I recommend that you minimize your use of property files or at least minimize the entries that go in them. Property file entries are disjointed from your application; they represent a bunch of global variables arbitrarily clustered together. You should prefer instead to carefully associate configurable values with the classes they impact. The better way to do this is through the use of a database.

Keep any property files small and simple or, better yet, eliminate them. Resist adding new entries.


If you must use property files, the Properties class contains methods for loading from an input stream that is either in key-value text format or in XML format. Do not try to dynamically update this file from your executing application. Instead, consider the Preferences API, discussed in the next section.

Preferences

For critical information that you must retain from application execution to execution, you should use a database or other trustworthy persistence mechanism. However, you often want the ability to quickly and simply store various bits of noncritical information. Usually this is in the area of user trivialities: preferred colors, window positioning, recently visited web sites, and so on. Were any of this information lost, it would be a nuisance to a user but would not prevent him or her from accurately completing work.

The Java Preferences API supplies a simple local persistence scheme for this data. PersistentFrameTest (shown after the next paragraph) demonstrates. The first test, testCreate, shows that the first time a frame is created, it uses default window positions. The test testMove moves a frame, then closes it. The test shows that subsequent frames use the last position of any PersistentFrame.

In order for PersistentFrameTest to work more than once, the setUp method sends the message clearPreferences to the frame. This method ensures that the PersistentFrame object uses default window placement settings.

 package sis.ui; import junit.framework.*; import java.awt.*; import java.util.prefs.BackingStoreException; import static sis.ui.PersistentFrame.*; public class PersistentFrameTest extends TestCase {    private PersistentFrame frame;    protected void setUp() throws BackingStoreException {       frame = new PersistentFrame();       frame.clearPreferences();       frame.initialize();       frame.setVisible(true);    }    protected void tearDown() {       frame.dispose();    }    public void testCreate() {       assertEquals(          new Rectangle(             DEFAULT_X, DEFAULT_Y, DEFAULT_WIDTH, DEFAULT_HEIGHT),          frame.getBounds());    }    public void testMove() {       int x = 600;       int y = 650;       int width = 150;       int height = 160;       frame.setBounds(x, y, width, height);       frame.dispose();       PersistentFrame frame2 = new PersistentFrame();       frame2.initialize();       frame2.setVisible(true);       assertEquals(          new Rectangle(x, y, width, height), frame2.getBounds());    } } 

The implementation in PersistentFrame:

 package sis.ui; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.prefs.*; public class PersistentFrame extends JFrame {    static final int DEFAULT_X = 100;    static final int DEFAULT_Y = 101;    static final int DEFAULT_WIDTH = 300;    static final int DEFAULT_HEIGHT = 400;    private static final String X = "x";    private static final String Y = "y";    private static final String WIDTH = "width";    private static final String HEIGHT = "height";    private Preferences preferences =       Preferences.userNodeForPackage(this.getClass());           // 1    public void initialize() {       int x = preferences.getInt(X, DEFAULT_X);                  // 2       int y = preferences.getInt(Y, DEFAULT_Y);       int width = preferences.getInt(WIDTH, DEFAULT_WIDTH);       int height = preferences.getInt(HEIGHT, DEFAULT_HEIGHT);       setBounds(x, y, width, height);       setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);       addWindowListener(new WindowAdapter() {          public void windowClosed(WindowEvent e) {             saveWindowPosition();          }       });    }    private void saveWindowPosition() {       Rectangle bounds = getBounds();       preferences.putInt(X, bounds.x);                           // 3       preferences.putInt(Y, bounds.y);       preferences.putInt(WIDTH, bounds.width);       preferences.putInt(HEIGHT, bounds.height);       try {          preferences.flush();                                    // 4       }       catch (BackingStoreException e) {          // not crucial; log message       }    }    // for testing    void clearPreferences() throws BackingStoreException {       preferences.clear();       preferences.flush();    } } 

You create a Preference object by passing a class name to one of two Preferences factory methods, userNodeForPackage or systemNodeForPackage, corresponding to the user and system trees of preference data. You might use the system preferences tree for application-specific information, such as the location of a properties file. You might use the user preferences tree for user-specific information, such as color choices.

Java stores preferences in trees that are analogous to file system directory tree structures. Depending on the platform (and specific implementation), the actual storage location of preferences could be an operating system registry, the file system, a directory server, or a database.

The PersistentFrame class uses the user preferences tree (see line 1). The frame initialization code attempts to read the preferences for the frame's bounds[13] (starting at line 2). Each call to getInt can contain a default value to use if no corresponding preference data exists.

[13] A Rectangle object specifies the bounds of a window. The (x, y) coordinate represent its origin, or upper-left corner. The height and width represent the extent of the window in pixels.

When the frame gets closed (by client code that invokes the dispose method or by other means), code in saveWindowPosition stores the current bounds of the frame using the Preferences API. You must flush the preferences object (line 4) to force a write to the persistent store. In addition to putInt (line 3) the Preferences class supplies other methods to support storing various types of data: String, boolean, byte[], double, float, and long.

The Preferences API provides methods for writing data to and reading data from a file in XML format.

System Environment

On rare occasions, you'll need to know information about your operating environment that the predefined system properties does not provide. Unix and Windows operating systems have environment variables, yet again a key-value pair scheme. From a command-line prompt, you view the environment variables using the command set.[14] From within Java, the System class provides two getEnv methods to help you retrieve these environment variables and their values.

[14] This works under Windows and under many Unix shells.

 Map<String,String> env = System.getenv(); for (Map.Entry entry: env.entrySet())    System.out.printf("%s->%s%n", entry.getKey(), entry.getValue()); System.out.println(System.getenv("SystemRoot")); // Unix: try "SHELL" 

Avoid depending on system environment variables. It will make it more difficult to port your application to another platform. Almost no environment variables are common between Windows and Unix. If you feel compelled to create your own system-wide environment variables, try to use the Java properties mechanism instead.

Executing Other Applications

You may on rare occasion need to kick off another operating system process. For example, you may want your application to bring up the system calculator application (calc.exe under Windows). Java will allow you to execute separate processes, but doing so is something you should avoid. It will usually tie you to a single operating system. In most cases you will then need to maintain conditional logic if you must handle multiple platforms.

There are two ways to create and execute separate processes under Java. The older technique requires you to obtain the singleton[15] instance of java.lang.Runtime using its class method getruntime. You can then pass a command-line string along with optional arguments to the exec method. J2SE 5.0 introduced a ProcessBuilder class that replaces this technique. You create a ProcessBuilder using the command-line string, then initiate the process by sending start to the ProcessBuilder object.

[15] The Runtime class is designed so that there can only be a single instance of Runtime in an executing Java application, therefore Runtime is an example of the singleton design pattern.

This works under Windows and under many Unix shells.

You will not see a console window when executing the new process. It is possible, but slightly tricky, to capture output from or siphon input to the new process. Java redirects the three standard IO streams (stdin, stdout, and stderr) to Java streams so that you can work with them. Usually you'll only need to capture output; that's what we'll concentrate on here.

The trickiness to capturing output comes from the fact that the buffer size of the operating system may be small. You must promptly manage data coming from each output stream. If you do not, your application is likely to hang as it executes.

 package sis.util; import junit.framework.TestCase; import java.util.*; public class CommandTest extends TestCase {    private static final String TEST_SINGLE_LINE = "testOneLine";    private static final String TEST_MULTIPLE_LINES = "testManyLines";    private static final String TEST_LOTS_OF_LINES = "testLotsLines";    private static Map<String, String> output =       new HashMap<String, String>();    private static final String COMMAND =                // 1       "java " +       "-classpath \"" + System.getProperty("java.class.path") + "\" " +       "sis.util.CommandTest %s";    private Command command;    static {                                            // 2       output.put(TEST_SINGLE_LINE, "a short line of text");       output.put(TEST_MULTIPLE_LINES, "line 1\\nline 2\\");       output.put(TEST_LOTS_OF_LINES, lotsOfLines());    }    static String lotsOfLines() {       final int lots = 1024;       StringBuilder lotsBuffer = new StringBuilder();       for (int i = 0; i < lots; i++)          lotsBuffer.append("" + i);       return lotsBuffer.toString();    }    public static void main(String[] args) {             // 3       String methodName = args[0];       String text = output.get(methodName);       // redirected output:       System.out.println(text);       System.err.println(text);    }    public void testSingleLine() throws Exception {       verifyExecuteCommand(TEST_SINGLE_LINE);    }    public void testMultipleLines() throws Exception {       verifyExecuteCommand(TEST_MULTIPLE_LINES);    }    public void testLotsOfLines() throws Exception {       verifyExecuteCommand(TEST_LOTS_OF_LINES);    }    private void verifyExecuteCommand(String text) throws Exception {       command = new Command(String.format(COMMAND, text));       command.execute();       assertEquals(output.get(text), command.getOutput());    } } 

CommandTest uses itself as the very command to test. Line 1 constructs a java command line to execute sis.util.CommandTest as a stand-alone application. It obtains the appropriate classpath by querying the system property java.class.path. The main method (line 3) supplies the code for the application.

The main method simply writes lines of output to stderr (System.err) and stdout (System.out). It uses a single argument, an arbitrary string that corresponds to a test method name, to determine what lines to write. The lines come from a map populated using a static initializer (line 2).

The Command class appears next. In line 1, you create a ProcessBuilder object using the command string. The ProcessBuilder start method kicks off the corresponding operating system process. Lines 4 and 6 show how you can extract the streams corresponding to stderr and stdout. Once you've obtained these streams, you can use them to capture output using a Buffered-Reader (see the collectOutput method at line 7). The trick to making all this work is to ensure that the process of collecting output executes in simultaneous threads, otherwise you run the risk of blocking or losing output. Thus, the code in collectErrorOutput (line 3) and collectOutput (line 5) creates and starts new Thread objects. After initiating the threads, code in execute waits until the process completes, using the Process method waitFor (line 2).

 package sis.util; import java.io.*; public class Command {    private String command;    private Process process;    private StringBuilder output = new StringBuilder();    private StringBuilder errorOutput = new StringBuilder();    public Command(String command) {       this.command = command;    }    public void execute() throws Exception {       process = new ProcessBuilder(command).start();          // 1       collectOutput();       collectErrorOutput();       process.waitFor();                                      // 2    }    private void collectErrorOutput() {                        // 3       Runnable runnable = new Runnable() {          public void run() {             try {                collectOutput(process.getErrorStream(),        // 4                              errorOutput);             } catch (IOException e) {                errorOutput.append(e.getMessage());             }          }       };       new Thread(runnable).start();    }    private void collectOutput() {                             // 5       Runnable runnable = new Runnable() {          public void run() {             try {                collectOutput(process.getInputStream(),        // 6                              output);             } catch (IOException e) {                output.append(e.getMessage());             }          }       };       new Thread(runnable).start();    }    private void collectOutput(                                // 7          InputStream inputStream, StringBuilder collector)           throws IOException {       BufferedReader reader = null;       try {          reader =              new BufferedReader(new InputStreamReader(inputStream));          String line;          while ((line = reader.readLine()) != null)             collector.append(line);       }       finally {          reader.close();       }    }    public String getOutput() throws IOException {       return output.toString();    }    public String getErrorOutput() throws IOException {       return output.toString();    }    public String toString() {       return command;    } } 

Avoid use of command-line redirection (> and <) or piping (|)these will be considered part of the command input string and will not be appropriately handled by the operating system.

The ProcessBuilder class allows you to pass new environment variables and values to the new process. You can also change the current working directory under which the process executes.

One thing that ProcessBuilder provides that the Runtime.exec technique does not is the ability to merge the stdout and stderr streams. Console applications often interweave these: You might get a prompt or other output message that comes from stdout, then an error message from stderr, then a stdout prompt, and so on. If stdout and stderr are separated into two streams, there is no way to determine the original chronological sequencing of the combined output. See the API documentation for details on how to control this feature.

Avoid use of things such as getEnv and ProcessBuilder that introduce dependencies on your operating environment.




Agile Java. Crafting Code with Test-Driven Development
Agile Javaв„ў: Crafting Code with Test-Driven Development
ISBN: 0131482394
EAN: 2147483647
Year: 2003
Pages: 391
Authors: Jeff Langr

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