Home | pfodApps/pfodDevices | WebStringTemplates | Java/J2EE | Unix | Torches | Superannuation | | About Us
 

Forward Logo (image)      

Java GUI Programming Tips and Guidelines
Unified Error/Information Messages and Help

by Matthew Ford
© Forward Computing and Control Pty. Ltd. NSW Australia
All rights reserved.

Historically help for error messages were referenced by number, (if any help was supplied at all). Having designed and coded a major Java application using this approach, I can attest to the overhead it imposes in keeping track of the error numbers and setting the correct number for the error at hand.

The Rules

When I was coding Parallel I wanted something that was easier to keep track of and more straight forward to use. I came up with the following rules:-

  1. Every distinct user informational message and every distinct error has its own class. All these classes are instances of java.lang.Throwable.

  2. Errors are instances of java.lang.Error or java.lang.Exception. User informational messages are not. They sub-class java.lang.Throwable directly.

  3. Every user information message and every error has its own associated on-line JavaHelp which is accessed via a topic ID. The topic ID is the full class name of this message/error.

  4. Each topic maps to a unique URL which is also based on the full class name. This URL is either a separate .html page for each topic or a separate bookmark for each topic, in a single .html page.

  5. There is one default topic which is returned if the given topic does not exist in the on-line help. This is part of the error recovery for the on-line help.

  6. A request to display help never throws an exception (although it may log an error message to a log file).

What Sun got Wrong with Exceptions

Exceptions (i.e. java.lang.Throwable) are a great feature of the Java language, however when Sun tried to impose additions constraints on the Throwables class hierarchy they got it wrong. Apart from the normal grouping of exceptions under various sub-classes, Sun tried to impose two additional distinctions.
i) a distinction between java.lang.Error and java.lang.Exception
ii) a distinction between checked and unchecked exceptions.

java.lang.Error versus java.lang.Exceptions

The Java docs for java.lang.Error states that
“ An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch.“

However this is not true. In modern multi-featured applications, users will not put up with the whole application failing just because an OutOfMemoryError or an AssertionError was thrown by one part of the code. Modern applications are expected to catch and try and recover from all errors. So You should at least try and log and recover from all java.lang.Error exceptions. If the whole application really fails you are no worse off and many times the application won't completely fail when a java.lang.Error is thrown. (see Error Recovery for more details).

This removes the artificial distinction between java.lang.Error and java.lang.Exception

Checked versus Unchecked Exceptions

Sun's tutorial on Java's Catch or Specify Requirement says of checked exceptions.
" If a method chooses not to catch an (checked) exception, the method must specify that it can throw that exception. Why did the Java designers make this requirement? Because any exception that can be thrown by a method is really part of the method's public programming interface: callers of a method must know about the exceptions that a method can throw in order to intelligently and consciously decide what to do about those exceptions. Thus, in the method signature you specify the exceptions that the method can throw."

This was a noble sentiment but it has failed in practice. As of Java V1.4, Sun introduced the chained exception facility to get around deficiencies in their policy of enforcing checked exceptions to be part of the method's public interface. Basically the problem is that those methods that are declared not to throw any (checked) exceptions break the principle of hiding the method's implementation. For example from the Java docs on java.lang.Throwable,

“suppose a persistent collection conforms to the Collection interface, and that its persistence is implemented atop java.io. Suppose the internals of the put method can throw an IOException. The implementation can communicate the details of the IOException to its caller while conforming to the Collection interface by wrapping the IOException in an appropriate unchecked exception.”

This means that, as of Java V1.4, you cannot rely on a Sun library “method's public programming interface” to tell you what exceptions it may throw. If the method is declared as throwing a particular class of exception it may actually be throwing another type of exception wrapped in the class of exception declared in the throws clause. On the other hand, it may be throwing an unchecked exception wrapping a checked exception because the method does not specify any checked exceptions.

This blurs the distinction between checked and unchecked exceptions. You need to catch both and look at what the getCause() returns to see what really happened.

Summary of the Present State of Throwables

As discussed above, the two constraints Sun tried to impose on the Throwables class hierarchy (Errors versus Exceptions and Checked versus Unchecked) are broken. The basic problem is that the language designers at Sun tried to specify precisely which errors you, the programmer, would be interested in. My contention is that in a modern application you should be interested in all errors that occur and that you should catch them, log them and try to recover from them. Under this approach there is no difference between Error and Exception or between Checked and Unchecked. These all represent application errors that you need to handle.

The distinction I do make is between application errors and user informational messages.

User Informational Messages

User informational messages are messages designed to let the user know why the application was not able to perform the requested task. There was no error as far as the operation of the application was concerned but something stopped it completing the task. Typical user problems are, the requested file does not exist, the input needs a number and the user did not enter a valid number, etc. In any non-trivial GUI application there will be a large number of such problems which need to be reported to the user. The typical process is :-

i) detect the problem (e.g. catch a FileNotFoundException or a NumberFormatException).
ii) do any local clean up that is necessary
iii) throw a user message to the top level of this task
iv) at the top level, do any other clean up necessary and let the user know what happened.

This is not very different from how application errors are handled. The main differences are that at point iii) the original error is re-thrown rather than a user message, and at point iv) application errors are logged for later reporting, while user messages don't need to be logged.

The question then is how to distinguish between application errors and user informational messages. Clearly the user informational messages should form a separate class hierarchy from application errors. Since application errors include sub-classes of both java.lang.Error and java.lang.Exception, user messages do not fit into either of these existing classes. This means user informational messages should sub-class directly from java.lang.Throwable.

The next section of this article will give examples of error and message classes and how to convert errors to messages when appropriate. The article will then go on to describe how to use these classes to generate unique topic IDs and URLs for use in setting up JavaHelp. Sample on-line help entries and map file entries are provided together with sample code for a dialog Help button and an ApplicationHelp class to show the help associated with the error or message.

Errors and Messages

As mentioned above, I define two general types of exceptions, errors and user informational messages. Both of these classes are instances of java.lang.Throwable

Errors are sub-classes of java.lang.Error or java.lang.Exception .

User Informational Messages are direct sub-classes of java.lang.Throwable.

The important point here is that all errors and user messages are instances of java.lang.Throwable so they all can be thrown back up the call stack. Also they both have the methods getMessage() and getLocalizedMessage() which can be used to return strings for logging and display.

This classification of errors versus messages makes it easy to decide when it is necessary to write a log file entry. Logging is discussed in How to Set Up Java Logging . Displaying the message to the user is covered in Dialog boxes and why you should not use them for messages.

As far as on-line help is concerned, both errors and messages can be handled by the same method which accepts a Throwable.

Example Message and Error Classes

A typical user informational message class is FileNotFound.

public class FileNotFound extends Throwable implements java.io.Serializable {
  public FileNotFound(String fileName) {
    super("Could not find this file\n"
       + fileName);
  }
  /** chaining constructor 
   *  t is the cause of this exception
   */
  public FileNotFound(Throwable t) {
    super(t.getMessage(),t);
  }
}

The corresponding error would be the IOException subclass, FileNotFoundException.

For your own application specific errors you can either create your own classes by sub-classing either from a checked or unchecked exception, depending on whether or not you want to specify the exception in the throws clause. I generally sub-class from a checked exception, such as java.lang.Exception , and specify my exception class in the throws clauses. I include a cause constructor so I can use this exception to wrap other exceptions.

public class ParallelException extends Exception implements java.io.Serializable {
  public ParallelException() {
  }

  public ParallelException(String errMsg) {
    super(errMsg);
  }

  /** chaining constructor 
   *  t is the cause of this exception
   */
  public ParallelException(String errMsg, Throwable t) {
    super(errMsg,t);
  }
}

Instead of using your own error class, at other times it is more convenient to catch the exception and wrap it in another exception of a class already specified by the method. I find this often happens when dealing with methods that throw an IOException . For example the lineFill() method below basically reads a line to fill the buffer and you would expect it to throw an IOException. However this particular implementation also does some processing on the bytes read in and can throw an UnsupportedEncodingException as well. Rather then pollute the method signature, the UnsupportedEncodingException is wrapped in an IOException before it is thrown.

/**
 * fill the inputBytes buffer
 */
private void lineFill() throws IOException {  

 ....      
 String line = reader.readLine();  // throws IOException 
 ....  
    
 // do some processing on the bytes
 try {
   inputBytes = line.getBytes(charsetName);
 } catch (UnsupportedEncodingException ex) {
   // throw this as an IOException to conform to throws clause
   throw (IOException)((new IOException("Unsupported encoding.")).initCause(ex));
 }
 ....  
} 

Note: Because of a lack of foresight when Sun implemented exception chaining, the IOException class does not have a chaining constructor. (PostScript: Sun has now added, in Java 6, an IOException constructor that takes a throwable.) So you need to create the new IOException and then access the Throwable.initCause() method to set the cause.
    (new IOException("Unsupported encoding.")).initCause(ex)
Finally you have to re-cast the result back to an IOException since initCause() returns a Throwable.

Swapping from Errors to Messages

This separation into errors and messages provides you with considerable flexibility in how you handle exceptions. For example a low level method might throw a FileNotFoundException. If the program was trying to open a file it needed for its operation then the FileNotFoundException is an error. However if the user had requested the program to open a file, then not finding the file is just an informational message. (The user never makes mistakes, see Thinking Like a User for more details). In this case you could catch this exception at some higher level and wrap it in FileNotFound message to present to the user.

Alternatively there may be situations were a low level routine throws what it thinks is an informational message but a higher level routine decides this is really an error. In that case you would wrap your message in a error and re-throw it.

Setting up the On-line help

Now that all the errors and user informational messages have their own individual classes and are all derive from java.lang.Throwable, we can set up the associated on-line help. Setting up the help consists of three parts:-
i) Writing the HTML help entries and assigning them unique URL's
ii) Creating entries in the JavaHelp map file to map unique topic ID's to the URL's
iii) Writing the code to initialize the help set and to show the help for an error or message.

For the basic details of setting up JavaHelp refer to the documentation contained in the download available at http://java.sun.com/products/javahelp/download_binary.html

Writing the HTML help entries and assigning them unique URL's.

Here are some samples of on-line help entries:-


au.com.forward.parallel.gui.messages.FileNotFound:
The file you are trying to access could not be found. Please check the directory name and spelling.
On non-Windows systems, capitalization is significant. That is 'test.txt' is a different file from 'TEST.TXT'.

java.io.IOException:
An error occurred while trying to read or write a file.
The message displayed with this error will give more information on the cause of this error.

Common causes of this error are:-


If all the on-line help for error and messages are in one file, you can construct a unique URL for each entry by using the full class name as a bookmark. i.e.

<P><A NAME="au.com.forward.parallel.gui.messages.FileNotFound">
<B>au.com.forward.parallel.gui.messages.FileNotFound</B></A>:
<BR>The file you are trying to access could not be found. Please check the directory name
and spelling.
<BR>On non-Windows systems, capitalization is significant. That is 'test.txt' is a 
different file form 'TEST.TXT'.</P>

Then the URL for this topic becomes the filePath#bookmark e.g.
html/error_messages.html#au.com.forward.parallel.gui.messages.FileNotFound

If the on-line help for each error and message are in a separate file then naming the files after the full class name and adding .html is sufficient. (Provided the full class name is not too long to be a valid file name, less then 60 characters seems safe.) e.g.
au.com.forward.parallel.gui.messages.FileNotFound.html

In this example all the on-line help for errors and messages are in a single file called error_messages.html

Creating entries in the JavaHelp map file to map unique topic ID's to the URL's

To create unique topic ID's for this help we again use the full class name. As you will see from the code in the next section, this makes it easy to determine the topic associated with a particular error or message.

Sample entries in the JavaHelp map file for these topics are:-

<mapID target="au.com.forward.parallel.gui.messages.FileNotFound" 
       url="html/error_messages.html#au.com.forward.parallel.gui.messages.FileNotFound"/>
<mapID target="java.io.IOException" 
       url="html/error_messages.html#java.io.IOException"/> 

An additional topic is added for the default message

<mapID target="no_help_available" url="html/no_help_available.html"/>

This is used when no topic matching the full class name could be found in the help map. This topic needs some associated text to display. In Parallel I use the following file:-


Parallel Error Messages V1.1

Unfortunately, the help you requested is not linked properly. Please save the Parallel.log file and email (with anti-spam) with the exact circumstance where help was unavailable.


Those of you familiar with J2EE web modules will see the similarity between this system and the one used there to map Java errors to html pages containing the user error message.

Writing the code to initialize the help set and to show the help for an error or message

Initialization of the help set.

The ApplicationHelp class handles initializing the help set and showing topic Ids. Initializing the help set is done in a static block at the top of the ApplicationHelp class. LogStdStreams, which is initialized in the main application class, is used to log any errors (see How to set up Java Logging for a more complete solution). No exceptions are thrown. Not being able to open the help set is considered a non-critical error (see Error Recovery). The StringUtils class is used for logging exceptions.

The static section of the ApplicationHelp class is:-

  /** the application help set, null if not found */
  private static HelpSet helpSet = null;

  /** the helpBroker, null if not set */
  private static HelpBroker helpBroker = null;

  /**  the name of the help set */
  private static final String HS_NAME = "au/com/forward/parallel/help/Parallel.hs";

  /** the help map, if this is null then there was an error initializing the help set */
  private static Map map = null;

  /** the default topic ID, if no help available for a topic */
  private static final String DEFAULT_ID = "no_help_available";

  /**
   * try and load the help set on startup
   */
  static {
    URL url = null;
    try {
      // load the help set from the same path as the main class.
      url = HelpSet.findHelpSet(
                au.com.forward.parallel.Parallel.class.getClassLoader(), 
                HS_NAME);
      if (url != null) {
        try {
          helpSet = new HelpSet(null, url);
          helpBroker = helpSet.createHelpBroker();
          map = helpSet.getCombinedMap();
        } catch (HelpSetException e) {
          // log an error here and continue, don't stop the application
          LogStdStreams.getLogStream().println("Cannot create HelpSet for " 
              + HS_NAME + "\n"+ StringUtils.toString(e));
        }
      } else {
        // log an error here and continue, don't stop the application
        LogStdStreams.getLogStream().println("Cannot create HelpSet for " + HS_NAME);
      }
    } catch (Exception ee) {
      // log an error here and continue, don't stop the application
      LogStdStreams.getLogStream().println("Unexpected exception occurred.\n"
                    + StringUtils.toString(ee));
    }
  }

If the help set is successfully found and loaded then map will be non-null. Otherwise an error is logged and map remains null.

Showing the on-line help

Typically you will add a Help button to your error or user message display. When the Help button is pressed you want it to open JavaHelp and display the associated topic.

Sample code to set up a Help button in a dialog box is shown below. See ExampleShowErrorMessage.java.txt for the complete code. This code relies on having initialized LogStdStreams so that System.err and System.out are redirected to the log file.

The code in showErrorMessage() creates a dialog box and gets the topic ID by calling getHelpContext(t). The method getHelpContext(t) just returns t.getClass().getName()

  public static void showErrorMessage(Throwable t) {
    // need to catch all errors here
    try {   
      JButton dismiss = new JButton("Continue");
      JButton help = new JButton("Help");    
      JOptionPane messageBox = new JOptionPane("",JOptionPane.INFORMATION_MESSAGE);
      
      String msg =  formatThrowableMessage(t); 
      messageBox.setMessage(msg);
      
      Object[] options = {dismiss,help};
      messageBox.setOptions(options);
      
      final JDialog dialog = messageBox.createDialog(frame,errMsgTitle);
      // if frame is null the messageBox is a top level component.
      
      final String helpID = getHelpContext(t);

      // add action listener for continue button
      dismiss.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          try {
            dialog.dispose();
          } catch (Throwable tex) {
            // something bad happened
            System.err.println("Caught exception in when trying to close showErrorMessaage()\n"
            +StringUtils.toString(tex));
          }
        }
      });
      
      // add action listener for help button
      help.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          SwingUtilities.invokeLater(new Runnable() {
            public void run() {
              try {
                  ApplicationHelp.showHelp(helpID);
              } catch (Throwable tex) {
                // something bad happened
                System.err.println("Caught exception in when trying to show help()\n"
                +StringUtils.toString(tex));
              }
            }
          });
        }
      });

  /**
   * returns the error message to be displayed for this exception
   *
   * could look up international property files etc
   * for now just show the message
   */
  private static String formatThrowableMessage(Throwable t) {
    return t.getMessage();
  }
    
  /**
   * returns the helpID associated with this exception
   *
   * will use the class name as the helpID
   */
  private static String getHelpContext(Throwable t) {
    return t.getClass().getName();
  }

The ApplicationHelp class handles initializing the help set and showing topic IDs. The showHelp() method, below, tries to display the help for a given topic. This method does not throw any errors. All errors are considered non-critical and are logged and recovered from. LogStdStreams, which is initialized in the main application class, is used to log the errors.

To display a help topic the showHelp(id) method is called

  /**
   *  Show help for this topicID
   *  This method does not throw any exceptions
   *
   *@param  id    help topicID 
   */
  public static void showHelp(String id) {
    try {
      if (map == null) {
        // no help set available
        return;
      }
  
      String mapID = DEFAULT_ID;  // set default no_help_available
  
      if (map.isValidID(id, helpSet)) {
        mapID = id; // ok found topic
      } else {
        String msg = "Help Id:'" + id + "' not found.";
        RuntimeException pex = new RuntimeException(msg);
        // don't throw it just log it with its stacktrace
        LogStdStreams.getLogStream().println(msg + "\n" + StringUtils.toString(pex));
      }
  
      try {
        helpBroker.setDisplayed(true);  // display Java help first
        helpBroker.setCurrentID(mapID); // then switch to this topic
      } catch (Exception e) {
        // log an error here and continue, don't stop the application
        LogStdStreams.getLogStream().println("Error setting help context\n"
                + StringUtils.toString(e));
      }
    } catch (Throwable t) {
      // log an error here and continue, don't stop the application
      LogStdStreams.getLogStream().println("Unexpected exception occurred.\n"
                    + StringUtils.toString(ee));    
    }
  }

If there was a problem initializing the help set, then all calls to showHelp() just quietly return. If a given topic can not be found in the help set then an error is logged and the a default topic no_help_available is shown instead. In any case no exceptions are thrown from this method.

Conclusion

In this article I have described a unified system of handling both error messages and user informational messages by sub-classing java.lang.Throwable. I have show how you can easily convert an error to an informational message, when appropriate, and how using separate exception classes for each distinct error and message makes it easy to set up associated on-line JavaHelp. The full class names provide the unique topic ID and are used to provide unique URLs. This significantly simplifies the maintenance of the help system when compared to the previous methods of using error numbers as identifiers.

Because all errors and messages are instances of java.lang.Throwable, they all have the methods getMessage() and getLocalizedMessage(). This simplifies internationalization of the error and message strings.


References: For more discussion on checked versus unchecked exceptions see The Trouble with Checked Exceptions and Java's checked exceptions were a mistake (and here's what I would like to do about it)



Contact Forward Computing and Control by email (with anti-spam)
©Copyright 1996-2003 Forward Computing and Control Pty. Ltd. ACN 003 669 994