[ Team LiB ] Previous Section Next Section

11.8 An Error Handler Dialog

Example 11-18 is another example of a custom dialog. This dialog is designed for use when an unrecoverable or unexpected exception occurs in a program. Instead of just printing a stack trace and exiting, as Java programs do by default, the static method ErrorHandler.displayThrowable( ) displays the exception detail message to the user. One of the buttons in the dialog allows the user to view the complete stack trace for the exception, and another button allows the user to report the exception to the program's developer (by POSTing a serialized copy of the exception object to a web server).

One of the interesting features of this example is that the dialog can change its layout and size. As illustrated in Figure 11-15, the dialog includes a Show Details button that expands the display to include a stack trace. Similarly, if the Send Report button is clicked, the dialog changes to display the text sent by the web server in response to the error report.

Figure 11-15. An ErrorHandler dialog, in three different states
figs/Jex3_1115.gif

The displayThrowable( ) method is assisted by two other static convenience methods that may also be useful on their own. First, getHTMLDetails( ) returns an HTML-formatted string describing the stack trace of a Throwable object. It illustrates two methods of Throwable that are new in Java 1.4. getStackTrace( ) returns an array of StackTraceElement objects (also new in Java 1.4) that provide programmatic access to the stack trace. getCause( ) is used with chained exceptions to obtain the Throwable that caused the current Exception.

The second static convenience method used by displayThrowable( ) is reportThrowable( ), which serializes the Throwable object and reports it to a web server, using an HTTP POST request. It does this with a java.net.URLConnection object, which was introduced and demonstrated in Chapter 5.

The dialog itself is implemented using a JOptionPane. This example is an advanced use of JOptionPane, however, and does not use one of the JOptionPane static methods for displaying a canned dialog. Instead, it uses a custom JLabel component to display the error message (and optional stack trace details). It also uses two custom JButton objects to implement the Show Details and Send Report buttons. The Exit button, however, is left to the JOptionPane itself. Because we use the JOptionPane( ) constructor rather than one of its static dialog display methods, we end up with a JOptionPane component, which needs to be placed within a JDialog in order to be displayed. Conveniently, the createDialog( ) method of JOptionPane creates a suitable JDialog object.

Example 11-18 includes an inner class named Test that purposely causes an exception (and uses Throwable.initCause( ) to chain the exception to another one) in order to demonstrate the error handling dialog. If you want to test the exception reporting feature, specify the URL to report it to on the command line. Note, however, that you cannot just use the URL of a regular static web page here—you'll probably get an HTTP 405 error message from the web server because static web pages can't be retrieved with a POST request. Instead, your URL must refer to a server-side script or servlet of some kind. Example 20-2 is a Java servlet designed to work hand-in-hand with this class. If you are not already familiar with servlets, however, don't feel that you have to go read Chapter 20 right now. If you have a PHP-enabled web server, the following simple PHP script will do. It does not decode or save the reported exception, but it does count the number of bytes in it and return a response.

$len = strlen($HTTP_RAW_POST_DATA);
echo "Thanks for reporting the error<br>" .
     "$len bytes received";

Simply place this PHP file on your web server somewhere and pass its URL to the ErrorHandler.Test program.

Example 11-18. ErrorHandler.java
package je3.gui;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.net.*;
import java.io.*;

/**
 * This class defines three static methods useful for coping with unrecoverable
 * exceptions and errors.  
 * 
 * getHTMLDetails( ) returns an HTML-formatted stack trace suitable for display.
 *
 * reportThrowable( ) serializes the exception and POSTs it to a web server
 * through a java.net.URLConnection.
 *
 * displayThrowable( ) displays the exception's message in a dialog box that
 *   includes buttons that invoke getHTMLDetails( ) and reportThrowable( ).
 *
 * This example demonstrates: StackTraceElement, chained exceptions, 
 * Swing dialogs with JOptionPane, object serialization, and URLConnection.
 */
public class ErrorHandler {
    /**
     * Display details about throwable in a simple modal dialog.  Title is the
     * title of the dialog box.  If submissionURL is not null, allow the user
     * to report the exception to that URL.  Component is the "owner" of the
     * dialog, and may be null for nongraphical applications.
     **/
    public static void displayThrowable(final Throwable throwable,
                                        String title,
                                        final String submissionURL,
                                        Component component)
    {
        // Get throwable class name minus the package name
        String className = throwable.getClass( ).getName( );
        className = className.substring(className.lastIndexOf('.')+1);

        // Basic error message is className plus exception message if any.
        String msg = throwable.getMessage( );
        final String basicMessage = className + ((msg != null)?(": "+msg):"");

        // Here is a JLabel to display the message.  We create the component
        // explicitly so we can manipulate it with the buttons.  Note final
        final JLabel messageLabel = new JLabel(basicMessage);

        // Here are buttons for the dialog.  They are final for use in 
        // the event listeners below.  The "Send Report" button is only
        // enabled if we have a URL to send a bug report to.
        final JButton detailsButton = new JButton("Show Details");
        final JButton reportButton = new JButton("Send Report");
        reportButton.setEnabled(submissionURL != null);

        // Our dialog will display a JOptionPane.  Note that we don't 
        // have to create the "Exit" button ourselves.  JOptionPane will
        // create it for us, and will cause it to close the dialog.
        JOptionPane pane =
            new JOptionPane(messageLabel, JOptionPane.ERROR_MESSAGE,
                            JOptionPane.YES_NO_OPTION, null,
                            new Object[  ] {detailsButton,reportButton,"Exit"});

        // This is the dialog box containing the pane.
        final JDialog dialog = pane.createDialog(component, title);

        // Add an event handler for the Details button
        detailsButton.addActionListener(new ActionListener( ) {
                public void actionPerformed(ActionEvent event) {
                    // Show or hide error details based on state of button.
                    String label = detailsButton.getText( );
                    if (label.startsWith("Show")) {
                        // JLabel can display simple HTML text
                        messageLabel.setText(getHTMLDetails(throwable));
                        detailsButton.setText("Hide Details");
                        dialog.pack( );  // Make dialog resize to fit details
                    }
                    else {
                        messageLabel.setText(basicMessage);
                        detailsButton.setText("Show Details");
                        dialog.pack( );
                    }
                }
            });


        // Event handler for the "Report" button.
        reportButton.addActionListener(new ActionListener( ) {
            public void actionPerformed(ActionEvent event) {
                try {
                    // Report the error, get response.  See below.
                    String response=reportThrowable(throwable, submissionURL);
                    // Tell the user about the report
                    messageLabel.setText("<html>Error reported to:<pre>" +
                                         submissionURL +
                                         "</pre>Server responds:<p>" +
                                         response + "</html>");
                    dialog.pack( );  // Resize dialog to fit new message
                    // Don't allow it to be reported again
                    reportButton.setText("Error Reported");
                    reportButton.setEnabled(false);
                }
                catch (IOException e) {  // If error reporting fails
                    messageLabel.setText("Error not reported: " + e);
                    dialog.pack( );
                }
            }
        });

        // Display the dialog modally.  This method will return only when the
        // user clicks the "Exit" button of the JOptionPane.
        dialog.show( );     
    }

    /**
     * Return an HTML-formatted stack trace for the specified Throwable, 
     * including any exceptions chained to the exception.  Note the use of 
     * the Java 1.4 StackTraceElement to get stack details.  The returned 
     * string begins with "<html>" and is therefore suitable for display in
     * Swing components such as JLabel.
     */
    public static String getHTMLDetails(Throwable throwable) {
        StringBuffer b = new StringBuffer("<html>");
        int lengthOfLastTrace = 1;  // initial value

        // Start with the specified throwable and loop through the chain of
        // causality for the throwable.
        while(throwable != null) {
            // Output Exception name and message, and begin a list 
            b.append("<b>" + throwable.getClass( ).getName( ) + "</b>: " +
                     throwable.getMessage( ) + "<ul>");
            // Get the stack trace and output each frame.  
            // Be careful not to repeat stack frames that were already reported
            // for the exception that this one caused.
            StackTraceElement[  ] stack = throwable.getStackTrace( );
            for(int i = stack.length-lengthOfLastTrace; i >= 0; i--) {
                b.append("<li> in " +stack[i].getClassName( ) + ".<b>" +
                         stack[i].getMethodName( ) + "</b>( ) at <tt>"+
                         stack[i].getFileName( ) + ":" +
                         stack[i].getLineNumber( ) + "</tt>");
            }
            b.append("</ul>");  // end list
            // See if there is a cause for this exception
            throwable = throwable.getCause( );
            if (throwable != null) {
                // If so, output a header
                b.append("<i>Caused by: </i>");
                // And remember how many frames to skip in the stack trace
                // of the cause exception
                lengthOfLastTrace = stack.length;  
            }
        }
        b.append("</html>"); 
        return b.toString( );
    }

    /**
     * Serialize the specified Throwable, and use an HttpURLConnection to POST
     * it to the specified URL.  Return the response of the web server.
     */
    public static String reportThrowable(Throwable throwable,
                                         String submissionURL) 
        throws IOException
    {
        URL url = new URL(submissionURL);        // Parse the URL
        URLConnection c = url.openConnection( );  // Open unconnected Connection
        c.setDoOutput(true);
        c.setDoInput(true);
        // Tell the server what kind of data we're sending
        c.addRequestProperty("Content-type",
                             "application/x-java-serialized-object");
        
        // This code might work for other URL protocols, but it is intended
        // for HTTP.  We use a POST request to send data with the request.
        if (c instanceof HttpURLConnection)
            ((HttpURLConnection)c).setRequestMethod("POST");
        
        c.connect( );  // Now connect to the server
        
        // Get a stream to write to the server from the URLConnection.
        // Wrap an ObjectOutputStream around it and serialize the Throwable.
        ObjectOutputStream out =
            new ObjectOutputStream(c.getOutputStream( ));
        out.writeObject(throwable);
        out.close( );
        
        // Now get the response from the URLConnection.  We expect it to be
        // an InputStream from which we read the server's response.
        Object response = c.getContent( );
        StringBuffer message = new StringBuffer( );
        if (response instanceof InputStream) {
            BufferedReader in =
              new BufferedReader(new InputStreamReader((InputStream)response));
            String line;
            while((line = in.readLine( )) != null) message.append(line);
        }
        return message.toString( );
    }

    // A test program to demonstrate the class
    public static class Test {
        public static void main(String[  ] args) {
            String url = (args.length > 0)?args[0]:null;
            try { foo( ); }
            catch(Throwable e) {
                ErrorHandler.displayThrowable(e, "Fatal Error", url, null);
                System.exit(1);
            }
        }
        // These methods purposely throw an exception
        public static void foo( ) { bar(null); }
        public static void bar(Object o) {
            try { blah(o); }
            catch(NullPointerException e) {
                // Catch the null pointer exception and throw a new exception
                // that has the NPE specified as its cause.
                throw (IllegalArgumentException)
                    new IllegalArgumentException("null argument").initCause(e);
            }
        }
        public static void blah(Object o) {
            Class c = o.getClass( );  // throws NPE if o is null
        }
    }
}
    [ Team LiB ] Previous Section Next Section