Home
| pfodApps/pfodDevices
| WebStringTemplates
| Java/J2EE
| Unix
| Torches
| Superannuation
|
| About
Us
|
Java GUI Programming Tips and
Guidelines
|
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.
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:-
Every distinct user informational message and every distinct error has its own class. All these classes are instances of java.lang.Throwable.
Errors are instances of java.lang.Error or java.lang.Exception. User informational messages are not. They sub-class java.lang.Throwable directly.
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.
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.
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.
A request to display help never throws an exception (although it may log an error message to a log file).
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.
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
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.
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 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.
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.
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
.
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.
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
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:-
the file could not be found,
the disk is full,
the file is already in use,
the file is set to ReadOnly.
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
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:-
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.
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
.
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.
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