/**
* Copyright 2005 Bushe Enterprises, Inc., Hopkinton, MA, USA, www.bushe.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bushe.swing.exception;
import java.util.List;
import java.util.ArrayList;
import java.util.Date;
import java.util.Properties;
import java.util.Iterator;
import java.util.StringTokenizer;
import java.awt.Component;
import java.awt.Frame;
import java.awt.BorderLayout;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
import java.awt.Insets;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Color;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.awt.event.KeyEvent;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JDialog;
import javax.swing.JSeparator;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.BorderFactory;
import javax.swing.KeyStroke;
import javax.swing.Action;
import javax.swing.AbstractAction;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JTextArea;
import javax.swing.JScrollPane;
import javax.swing.JButton;
import javax.swing.Icon;
import javax.swing.UIManager;
import javax.swing.SwingUtilities;
//import org.jdesktop.jdic.desktop.Desktop;
//import org.jdesktop.jdic.desktop.Message;
/**
* A dialog that displays exception that occur in the AWT Event Queue.
* <p>
* It is fully customizable.
* @author Michael Bushe michael@bushe.com
*/
public class ExceptionDialog extends JDialog {
public static final int PREFERRED_WIDTH = 600;
private Throwable throwable;
private Component detailsComponent;
private JSeparator separator;
private String emailAddress;
public ExceptionDialog(Frame ownerFrame, Throwable t, boolean modal) {
this(ownerFrame, t, modal, null);
}
public ExceptionDialog(Frame ownerFrame, Throwable t, boolean modal, String emailAddress) {
super(ownerFrame, "Application Error", modal);
this.throwable = t;
this.emailAddress = emailAddress;
setupClose();
initUI(t);
}
/**
* Called during construction to initialze the components and calls doLayout() with them.
* <p>
* You can overridde this method to create your own components and layout.
* @param t the throwable to show
*/
protected void initUI(Throwable t) {
detailsComponent = createDetailsComponent(t);
JLabel errorIconLabel = createErrorIconLabel();
JLabel messageLabel = createErrorMessageComponent();
Component buttonPanel = createButtonPanelComponent();
JLabel titleLabel = createTitleComponent();
separator = createSeparator();
doLayout(errorIconLabel, titleLabel, messageLabel, buttonPanel, detailsComponent, separator);
}
/**
* Layouts the component on the context pane.
* @param errorIconLabel the icon gotten from createErrorIconLabel()
* @param messageLabel the
* @param buttonPanel
* @param detailsPanel
*/
protected void doLayout(JLabel errorIconLabel, JLabel titleLabel, JLabel messageLabel, Component buttonPanel,
Component detailsPanel, Component separator) {
JPanel content = new JPanel(new BorderLayout());
JPanel centerContentPanel = new JPanel(new GridBagLayout());
content.add(centerContentPanel, BorderLayout.CENTER);
centerContentPanel.setBorder(BorderFactory.createEmptyBorder(10, 5, 10, 5));
setContentPane(content);
GridBagConstraints gbc = new GridBagConstraints();
Insets insets0t10l5b5r = new Insets(0, 10, 5, 5);
gbc.gridx = 0;
gbc.gridy = 0;
gbc.gridheight = 3;
gbc.anchor = GridBagConstraints.NORTH;
centerContentPanel.add(errorIconLabel, gbc);
gbc.gridx = 1;
gbc.gridy = 0;
gbc.gridheight = 1;
gbc.insets = insets0t10l5b5r;
gbc.anchor = GridBagConstraints.WEST;
centerContentPanel.add(titleLabel, gbc);
gbc.gridx = 1;
gbc.gridy = 1;
gbc.insets = new Insets(0, 20, 5, 5);
centerContentPanel.add(messageLabel, gbc);
gbc.gridx = 0;
gbc.gridy = 2;
gbc.gridwidth = 2;
gbc.insets = new Insets(0, 0, 5, 0);
centerContentPanel.add(buttonPanel, gbc);
gbc.gridx = 0;
gbc.gridy = 3;
gbc.gridwidth = 2;
gbc.insets = new Insets(0, 5, 5, 0);
centerContentPanel.add(separator, gbc);
gbc.gridx = 0;
gbc.gridy = 4;
gbc.gridwidth = 2;
gbc.insets = new Insets(0, 5, 5, 0);
gbc.weightx = 1.0;
gbc.weighty = 1.0;
gbc.fill = GridBagConstraints.BOTH;
centerContentPanel.add(detailsPanel, gbc);
}
/**
* @return a JSeparator that gets swapped out with the detail pane.
*/
protected JSeparator createSeparator() {
JSeparator separator = new JSeparator();
separator.setPreferredSize(new Dimension(PREFERRED_WIDTH, 3));
if (getDefaultDetailsVisible()) {
separator.setVisible(false);
}
return separator;
}
/**
* Ensures the dialog disposes on close, escape, or X.
*/
protected void setupClose() {
setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
Action actionListener = new AbstractAction() {
public void actionPerformed(ActionEvent actionEvent) {
setVisible(false);
}
};
InputMap inputMap = getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
inputMap.put(stroke, "ESCAPE");
getRootPane().getActionMap().put("ESCAPE", actionListener);
}
/**
* The component that shows the component details message. Calles getComponentDetailMessage(throwable) for the text.
* @param t the throwable being displayed
* @return a scroll pane with a text area by default.
*/
protected Component createDetailsComponent(Throwable t) {
JTextArea throwableTextArea = new JTextArea(getDetailComponentMessageText(), 25, 80);
throwableTextArea.setEditable(false);
JScrollPane scrollPane = new JScrollPane(throwableTextArea);
if (!getDefaultDetailsVisible()) {
scrollPane.setVisible(false);
}
scrollPane.setPreferredSize(new Dimension(PREFERRED_WIDTH, scrollPane.getPreferredSize().height));
return scrollPane;
}
/**
* @return the string in the details message pane, returns determineDetailMessage(throwable) by default
*/
protected String getDetailComponentMessageText() {
return determineDetailMessage(throwable);
}
/**
* @return the string in mail message, returns determineDetailMessage(throwable) by default
*/
protected String getEmailMessageText() {
return determineDetailMessage(throwable);
}
/**
* Get the panel of control buttons.
* @return by default a panel with OK, Copy, and, if emailAddress is not null, Email
*/
protected JComponent createButtonPanelComponent() {
JComponent buttonPanel = new JPanel(new GridBagLayout());
JPanel leftButtonPanel = new JPanel();
//Why am I not using BasicAction? Because this class should only depend on java or org.bushe.swing
AbstractAction action = new AbstractAction("OK") {
public void actionPerformed(ActionEvent e) {
close();
}
};
JButton okButton = new JButton(action);
leftButtonPanel.add(okButton);
JButton copyButton = new JButton("Copy");
copyButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
copy();
}
});
leftButtonPanel.add(copyButton);
if (AWTExceptionHandler.getErrorEmailAddress() != null) {
JButton emailButton = new JButton("Email");
emailButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
// email();
}
});
leftButtonPanel.add(emailButton);
}
if (detailsComponent != null) {
boolean defaultDetailsVisible = getDefaultDetailsVisible();
final JButton detailsButton = new JButton(defaultDetailsVisible?"Details >>":"<< Details");
detailsButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if ("<< Details".equals(detailsButton.getText())) {
detailsComponent.setVisible(true);
separator.setVisible(false);
detailsButton.setText("Details >>");
} else {
detailsComponent.setVisible(false);
separator.setVisible(true);
detailsButton.setText("<< Details");
}
pack();
}
});
GridBagConstraints gbc = new GridBagConstraints();
gbc.anchor = GridBagConstraints.WEST;
buttonPanel.add(leftButtonPanel, gbc);
gbc.gridx = 1;
gbc.anchor = GridBagConstraints.EAST;
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1.0;
JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
rightPanel.add(detailsButton);
buttonPanel.add(rightPanel, gbc);
buttonPanel.setBackground(Color.BLACK);
}
getRootPane().setDefaultButton(okButton);
return buttonPanel;
}
/**
* Should the details component be visible by default?
* @return flase by default
*/
protected boolean getDefaultDetailsVisible() {
return false;
}
/**
* @return a JLabel with the getErrorIcon() for an icon.
*/
protected JLabel createErrorIconLabel() {
return new JLabel(getErrorIcon());
}
/**
* @return a JLabel with "The following error occurred:"
*/
protected JLabel createTitleComponent() {
JLabel label = new JLabel("The following error occurred:");
return label;
}
/**
*@return a JLabel with the text of getMessageText(throwable)
*/
protected JLabel createErrorMessageComponent() {
return new JLabel(getMessageText(throwable));
}
/**
* @return the TSOpenPane's error icon from the look and feel (UIManager.getIcon("OptionPane.errorIcon"))
*/
protected Icon getErrorIcon() {
return UIManager.getIcon("OptionPane.errorIcon");
}
/**
* Called to copy the text in the details component (actually getDetailComponentMessageText()) to the system
* clipboard.
*/
protected void copy() {
StringSelection ss = new StringSelection(getDetailComponentMessageText());
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, null);
}
/**
* Called when the user clicks the email button.
protected void email() {
if (emailAddress != null) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
try {
Message emailMessage = new Message();
List to = new ArrayList();
to.add(emailAddress);
emailMessage.setToAddrs(to);
emailMessage.setSubject(getErrorEmailSubject());
emailMessage.setBody(getEmailMessageText());
Desktop.mail(emailMessage);
} catch (Throwable ex) {
//May catch DesktopException or ClassNotFoundException, etc.
throw new RuntimeException("Could not email previous error. Likely a Java Desktop Integration configuraiton issue. "+ex, ex);
}
}
});
}
}
*/
/**
* Computes the detail shown in the details component.
* @param t the throwable we are throwing an exception for
* @return by default a timestamp, the root Cause and stack, and full stack
*/
protected String determineDetailMessage(Throwable t) {
String msg = ""+new Date()+"\n";
Throwable root = getRootCause(t);
if (root == t) {
msg = msg + t.getMessage()+"\n";
msg = msg + AWTExceptionHandler.stackTraceToString(t);
} else {
msg = msg + "Root Cause:"+ root.getMessage()+"\n";
msg = msg + AWTExceptionHandler.stackTraceToString(root);
msg = msg + "Full Trace:"+ t.getMessage()+"\n";
msg = msg + AWTExceptionHandler.stackTraceToString(t);
}
String jvmProps = "\n" + "JVM properties:";
Properties props = System.getProperties();
Iterator keyIt = props.keySet().iterator();
while (keyIt.hasNext()) {
Object o = keyIt.next();
jvmProps = jvmProps + "\n" + o +"="+props.get(o);
}
return msg + jvmProps;
}
/**
* Gets the message from the root cause, breaks it up into an 80 character
* wide html message.
* @param t the throwable.
* @return a nice message for a throwable, ready to be JLabel'ed.
*/
protected String getMessageText(Throwable t) {
if (t == null) {
return "No throwable available.";
}
t = getRootCause(t);
String msg = "<html>";
String fullMessage = t.getMessage();
if (fullMessage == null) {
msg = msg + "No message available";
} else if (fullMessage.length() < 80) {
msg = msg + fullMessage;
} else {
//Break the message up into 80 character wide bits
String line = "";
StringTokenizer tok = new StringTokenizer(fullMessage, " \throwable\n\r\f", true);
while (tok.hasMoreTokens()) {
String token = tok.nextToken();
if (line.length() + token.length() > 80) {
msg = msg + line+"<br>";
line = "";
} else {
line = line + token;
}
}
msg = msg + line;
}
return msg + "</html>";
}
/**
* @return return the subject of the error email. "Application Error" by default.
*/
protected String getErrorEmailSubject() {
return "Application Error";
}
/**
* Called on window close to dispose.
*/
protected void close() {
dispose();
}
//Isn'throwable this in some later JDK API? I know I saw it somewhere...
private Throwable getRootCause(Throwable t) {
while(t.getCause() != null) {
t = t.getCause();
}
return t;
}
public void setEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
}
}