/*
* RapidMiner
*
* Copyright (C) 2001-2014 by RapidMiner and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapidminer.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.gui.tools.dialogs;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Collection;
import java.util.LinkedList;
import java.util.Locale;
import javax.swing.AbstractButton;
import javax.swing.DefaultListModel;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JToggleButton;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.apache.xmlrpc.client.XmlRpcClient;
import com.rapidminer.RapidMiner;
import com.rapidminer.RapidMiner.ExecutionMode;
import com.rapidminer.gui.MainFrame;
import com.rapidminer.gui.RapidMinerGUI;
import com.rapidminer.gui.dialog.BugZillaAssistant;
import com.rapidminer.gui.tools.ExtendedJScrollPane;
import com.rapidminer.gui.tools.ProgressThread;
import com.rapidminer.gui.tools.ResourceAction;
import com.rapidminer.gui.tools.SwingTools;
import com.rapidminer.gui.tools.components.LinkButton;
import com.rapidminer.operator.Operator;
import com.rapidminer.operator.UserError;
import com.rapidminer.repository.RepositoryException;
import com.rapidminer.tools.I18N;
import com.rapidminer.tools.Tools;
import com.rapidminer.tools.XmlRpcHandler;
/**
* The error message dialog. Several buttons are provided in addition to the error message. Details about the exception
* can be shown and an edit button can jump to the source code if an editor was defined in the properties / settings. In
* case of a non-expected error (i.e. all non-user errors) a button for sending a bug report is also provided.
*
* @author Ingo Mierswa, Simon Fischer, Tobias Malbrecht, Marco Boeck
*/
public class ExtendedErrorDialog extends ButtonDialog {
private static final long serialVersionUID = -8136329951869702133L;
private static final int SIZE = ButtonDialog.DEFAULT_SIZE;
private final JButton editButton = new JButton("Edit");
private JButton sendReport;
private Throwable error;
private final JComponent mainComponent = new JPanel(new BorderLayout());
/**
* Creates a dialog with the internationalized I18n-message from the given key and a panel for detailed stack trace.
*
* @param key
* the I18n-key which will be used to display the internationalized message
* @param error
* the exception associated to this message
* @param arguments
* additional arguments for the internationalized message, which replace <code>{0}</code>,
* <code>{1}</code>, etcpp.
*/
public ExtendedErrorDialog(String key, Throwable error, Object... arguments) {
this(key, error, false, arguments);
}
/**
* Creates a dialog with the internationalized I18n-message from the given key and a panel for detailed stack trace.
*
* @param key
* the I18n-key which will be used to display the internationalized message
* @param error
* the exception associated to this message
* @param displayExceptionMessage
* indicates if the exception message can be shown using the details button.
* @param arguments
* additional arguments for the internationalized message, which replace <code>{0}</code>,
* <code>{1}</code>, etcpp.
*/
public ExtendedErrorDialog(String key, Throwable error, boolean displayExceptionMessage, Object... arguments) {
super("error." + key, true, arguments);
this.error = error;
boolean hasError = (error != null);
JComponent detailedPane = hasError ? createDetailPanel(error) : null;
if ((error != null) && (error instanceof UserError) && (((UserError) error).getOperator() != null)) {
final String opName = ((UserError) error).getOperator().getName();
mainComponent.add(new LinkButton(new ResourceAction("show_offending_operator", opName) {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
MainFrame mainFrame = RapidMinerGUI.getMainFrame();
mainFrame.selectOperator(mainFrame.getProcess().getOperator(opName));
}
}), BorderLayout.NORTH);
}
layoutDefault(mainComponent, SIZE, getButtons(hasError && displayExceptionMessage, isBugReportException(error), detailedPane, error));
}
/**
* Creates a dialog with the internationalized I18n-message from the given key.
*
* @param key
* the I18n-key which will be used to display the internationalized message
* @param errorMessage
* the error message associated to this message
* @param arguments
* additional arguments for the internationalized message, which replace <code>{0}</code>,
* <code>{1}</code>, etcpp.
*/
public ExtendedErrorDialog(String key, String errorMessage, Object... arguments) {
super("error." + key, true, arguments);
boolean hasError = (errorMessage != null) && !errorMessage.isEmpty();
JScrollPane detailedPane = hasError ? createDetailPanel(errorMessage) : null;
layoutDefault(mainComponent, SIZE, getButtons(hasError, false, detailedPane, null));
}
@Override
protected Icon getInfoIcon() {
String configuredIcon = I18N.getMessageOrNull(I18N.getGUIBundle(), getKey() + ".icon");
if (configuredIcon != null) {
return super.getInfoIcon();
}
return SwingTools.createIcon("48/" + I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.error.icon"));
}
/**
* Creates a Panel for the error details and attaches the exception to it, but doesn't add the Panel to the dialog.
*
* @param error
* @return
*/
private JComponent createDetailPanel(Throwable error) {
StackTraceList stl = new StackTraceList(error);
JScrollPane detailPane = new ExtendedJScrollPane(stl);
detailPane.setPreferredSize(new Dimension(getWidth(), 200));
return detailPane;
}
/**
* Creates a Panel for the error details and attaches the error message to it, but doesn't add the Panel to the
* dialog.
*
* @param squaredError
* @return
*/
private JScrollPane createDetailPanel(String errorMessage) {
JTextArea textArea = new JTextArea(errorMessage);
textArea.setLineWrap(true);
textArea.setEditable(false);
JScrollPane detailPane = new ExtendedJScrollPane(textArea);
detailPane.setPreferredSize(new Dimension(getWidth(), 200));
return detailPane;
}
/**
* Adds all necessary buttons to the dialog.
*
* @param hasError
* @param isBug
* @param detailedPane
* the Panel which will be shown, if the user clicks on the 'Show Details' Button
* @param error
* The error occurred
* @return
*/
private Collection<AbstractButton> getButtons(boolean hasError, boolean isBug, final JComponent detailedPane, final Throwable error) {
Collection<AbstractButton> buttons = new LinkedList<AbstractButton>();
if (hasError && !(error instanceof RepositoryException)) {
final JToggleButton showDetailsButton = new JToggleButton(I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.error.show_details.label"), SwingTools.createIcon("24/" + I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.error.show_details.icon")));
showDetailsButton.setSelected(false);
showDetailsButton.addActionListener(new ActionListener() {
private boolean detailsShown = false;
@Override
public void actionPerformed(ActionEvent e) {
if (detailsShown) {
int width2 = ExtendedErrorDialog.this.getWidth();
mainComponent.remove(detailedPane);
ExtendedErrorDialog.this.setPreferredSize(new Dimension(width2, ExtendedErrorDialog.this.getHeight() - 150));
pack();
} else {
int width2 = ExtendedErrorDialog.this.getWidth();
mainComponent.add(detailedPane, BorderLayout.CENTER);
ExtendedErrorDialog.this.setPreferredSize(new Dimension(width2, ExtendedErrorDialog.this.getHeight() + 150));
pack();
}
detailsShown = !detailsShown;
}
});
buttons.add(showDetailsButton);
}
if (isBug) {
sendReport = new JButton(new ResourceAction("send_bugreport") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
// in case of UserError, ask if the user really wants to send a bugreport
// because it's likely not a bug
if (error instanceof UserError) {
if (SwingTools.showConfirmDialog("send_bugreport.confirm", ConfirmDialog.YES_NO_OPTION) == ConfirmDialog.NO_OPTION) {
return;
}
}
// create bug report window and connect to BugZilla
new ProgressThread("connect_to_bugzilla", false) {
@Override
public void run() {
sendReport.setEnabled(false);
final BugZillaAssistant bugAst;
getProgressListener().setTotal(100);
getProgressListener().setCompleted(10);
char[] pw = new char[] { '!', 'z', '4', '8', '#', 'H', 'c', '2', '$', '%', 'm', ')', '9', '+', '*', '*' };
String email = "bugs@rapid-i.com";
try {
XmlRpcClient rpcClient = XmlRpcHandler.login(XmlRpcHandler.BUGZILLA_URL, email, pw);
getProgressListener().setCompleted(20);
bugAst = new BugZillaAssistant(this, error, rpcClient);
} catch (Exception e) {
SwingTools.showVerySimpleErrorMessage("bugreport_xmlrpc_init_error");
return;
} finally {
for (int i = 0; i < pw.length; i++) {
pw[i] = 0;
}
getProgressListener().complete();
sendReport.setEnabled(true);
}
if (!isCancelled()) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
// if operator from import group is present, ask the user to include the data in the bug report
if (RapidMinerGUI.getMainFrame() != null && RapidMinerGUI.getMainFrame().getProcess() != null) {
for (Operator op : RapidMinerGUI.getMainFrame().getProcess().getAllOperators()) {
if (op.getOperatorDescription().getGroup().toLowerCase(Locale.ENGLISH).contains("import") ||
op.getName().toLowerCase(Locale.ENGLISH).equals("retrieve")) {
SwingTools.showMessageDialog("send_bugreport.import_operator_message");
break;
}
}
}
bugAst.setVisible(true);
}
});
} else {
bugAst.dispose();
}
}
}.start();
}
});
// don't show "Send bugreport" button if this dialog is shown when RM is only embedded
if (!RapidMiner.getExecutionMode().equals(ExecutionMode.EMBEDDED_WITH_UI)) {
buttons.add(sendReport);
}
}
buttons.add(makeCloseButton());
return buttons;
}
/**
* Returns <code>true</code> if this is a "real" bug, <code>false</code> otherwise.
*
* @param t
* @return
*/
private boolean isBugReportException(Throwable t) {
return (t instanceof RuntimeException) || (t instanceof Error);
//return !(t instanceof NoBugError || t instanceof XMLException || t instanceof RepositoryException);
}
/**
* Overrides the {@link ButtonDialog} method to add the exception message to the internationalized message
*/
@Override
protected String getInfoText() {
if (error != null) {
StringBuilder infoText = new StringBuilder();
infoText.append("<div>");
infoText.append(super.getInfoText());
infoText.append("</div>");
// if already arguments are given, we can expect already a detailed error message
if (arguments.length == 0 && error.getMessage() != null && error.getMessage().length() > 0) {
infoText.append("<br/>");
infoText.append(Tools.escapeHTML(error.getMessage()));
}
Throwable cause = error.getCause();
if (cause != null) {
String message = Tools.escapeHTML(cause.getMessage());
if (message == null) {
message = cause.toString();
}
if (!"null".equals(message)) {
infoText.append("<br/>");
infoText.append("<strong>" + I18N.getMessage(I18N.getGUIBundle(), "gui.dialog.error.process_failed_reason.message") + " </strong>");
infoText.append(message);
}
}
return infoText.toString();
} else {
return super.getInfoText();
}
}
private static class FormattedStackTraceElement {
private final StackTraceElement ste;
private FormattedStackTraceElement(StackTraceElement ste) {
this.ste = ste;
}
@Override
public String toString() {
return " " + ste;
}
}
private class StackTraceList extends JList {
private static final long serialVersionUID = -2482220036723949144L;
public StackTraceList(Throwable t) {
super(new DefaultListModel());
setFont(getFont().deriveFont(Font.PLAIN));
setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
appendAllStackTraces(t);
addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
if (getSelectedIndex() >= 0) {
if (!(getSelectedValue() instanceof FormattedStackTraceElement)) {
editButton.setEnabled(false);
} else {
editButton.setEnabled(true);
}
} else {
editButton.setEnabled(true);
}
}
});
}
private DefaultListModel model() {
return (DefaultListModel) getModel();
}
private void appendAllStackTraces(Throwable throwable) {
while (throwable != null) {
appendStackTrace(throwable);
throwable = throwable.getCause();
if (throwable != null) {
model().addElement("");
model().addElement("Cause");
}
}
}
private void appendStackTrace(Throwable throwable) {
model().addElement("Exception: " + throwable.getClass().getName());
model().addElement("Message: " + throwable.getMessage());
model().addElement("Stack trace:" + Tools.getLineSeparator());
for (int i = 0; i < throwable.getStackTrace().length; i++) {
model().addElement(new FormattedStackTraceElement(throwable.getStackTrace()[i]));
}
}
}
}