pdfbox-trunk/debugger/src/main/java/org/apache/pdfbox/debugger/ui/ErrorDialog.java
/*
 * Copyright 2015 The Apache Software Foundation.
 *
 * 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.apache.pdfbox.debugger.ui;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.Arrays;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JEditorPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.KeyStroke;
import javax.swing.ScrollPaneConstants;
import javax.swing.WindowConstants;

/**
 * A dialog to display a runtime exception stack trace.
 *
 * @author Pinaki Poddar
 *
 * Modified from ErrorDialog.java and SwingHelper.java of the Apache OpenJPA
 * <a href="https://svn.apache.org/repos/asf/openjpa/trunk/openjpa-examples/openbooks/src/main/java/jpa/tools/swing/">jpa.tools.swing
 * package</a>.
 *
 */
@SuppressWarnings({"serial","squid:MaximumInheritanceDepth"})
public class ErrorDialog extends JDialog
{
    private static final List<String> FILTERS = Arrays.asList(
            "java.awt.",
            "javax.swing.",
            "sun.reflect.",
            "java.util.concurrent.");
    private static final Dimension MESSAGE_SIZE = new Dimension(800, 200);
    private static final Dimension STACKTRACE_SIZE = new Dimension(800, 300);
    private static final Dimension TOTAL_SIZE = new Dimension(800, 500);
    private static final int BORDER_SIZE = 20;

    private static final String NEWLINE = "\r\n";
    private static final String INDENT = "    ";

    private boolean showingDetails;
    private boolean isFiltering = true;
    private final JComponent message;
    private final JComponent main;
    private JScrollPane details;
    private JTextPane stacktrace;
    private final Throwable error;

    /**
     * Creates a modal dialog to display the given exception message.
     *
     * @param t the exception to display
     */
    public ErrorDialog(Throwable t)
    {
        this(null, null, t);
    }

    /**
     * Creates a modal dialog to display the given exception message.
     *
     * @param owner if non-null, then the dialog is positioned (centered) w.r.t. this component
     * @param t the exception to display
     */
    public ErrorDialog(JComponent owner, Throwable t)
    {
        this(owner, null, t);
    }

    /**
     * Creates a modal dialog to display the given exception message.
     *
     * @param owner if non-null, then the dialog is positioned (centered) w.r.t. this component
     * @param icon the icon to display
     * @param t the exception to display
     */
    public ErrorDialog(JComponent owner, Icon icon, Throwable t)
    {
        setTitle(t.getClass().getName());
        setModal(true);
        if (icon instanceof ImageIcon)
        {
            setIconImage(((ImageIcon) icon).getImage());
        }
        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
        error = t;
        message = createErrorMessage(error);
        main = createContent();
        getContentPane().add(main);

        pack();
        position(this, owner);
    }

    /**
     * Position the given component at the center of the given parent component or physical screen.
     *
     * @param c the component to be positioned
     * @param parent the component whose center will match the center of the given component. If
     * null, the given component will match the screen center.
     *
     */
    static void position(Component c, Component parent)
    {
        Dimension d = c.getPreferredSize();
        if (parent == null)
        {
            Dimension s = Toolkit.getDefaultToolkit().getScreenSize();
            c.setLocation(s.width / 2 - d.width / 2, s.height / 2 - d.height / 2);
        }
        else
        {
            Point p = parent.getLocationOnScreen();
            int pw = parent.getWidth();
            int ph = parent.getHeight();
            c.setLocation(p.x + pw / 2 - d.width / 2, p.y + ph / 2 - d.height / 2);
        }
    }

    /**
     * Creates the display with the top-level exception message followed by a pane (that toggles)
     * for detailed stack traces.
     */
    final JComponent createContent()
    {
        final JButton showDetails = new JButton("Show Details >>");
        showDetails.addActionListener(e ->
        {
            if (showingDetails)
            {
                main.remove(details);
                main.validate();
                main.setPreferredSize(MESSAGE_SIZE);
            }
            else
            {
                if (details == null)
                {
                    details = createDetailedMessage();
                    StringBuilder buffer = new StringBuilder();
                    stacktrace.setText(generateStackTrace(error, buffer).toString());
                    stacktrace.setCaretPosition(0);
                    stacktrace.setBackground(main.getBackground());
                    stacktrace.setPreferredSize(STACKTRACE_SIZE);
                }
                main.add(details, BorderLayout.CENTER);
                main.validate();
                main.setPreferredSize(TOTAL_SIZE);
            }
            showingDetails = !showingDetails;
            showDetails.setText(showingDetails ? "<< Hide Details" : "Show Details >>");
            ErrorDialog.this.pack();
        });
        JPanel messagePanel = new JPanel();

        final JCheckBox filter = new JCheckBox("Filter stack traces");
        filter.setSelected(isFiltering);
        filter.addActionListener(e ->
        {
            isFiltering = filter.isSelected();
            StringBuilder buffer = new StringBuilder();
            stacktrace.setText(generateStackTrace(error, buffer).toString());
            stacktrace.setCaretPosition(0);
            stacktrace.repaint();
        });
        message.setBackground(messagePanel.getBackground());
        JPanel buttonPanel = new JPanel();
        buttonPanel.add(Box.createHorizontalStrut(BORDER_SIZE));
        buttonPanel.add(showDetails);
        buttonPanel.add(filter);
        buttonPanel.add(Box.createHorizontalGlue());
        messagePanel.setLayout(new BorderLayout());
        messagePanel.setBorder(BorderFactory.createEmptyBorder(BORDER_SIZE, BORDER_SIZE, BORDER_SIZE, BORDER_SIZE));
        messagePanel.add(message, BorderLayout.CENTER);
        messagePanel.add(buttonPanel, BorderLayout.SOUTH);
        messagePanel.setPreferredSize(MESSAGE_SIZE);

        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());
        panel.add(messagePanel, BorderLayout.NORTH);
        
        // allow closing with ESC
        ActionListener actionListener = actionEvent -> dispose();
        KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
        panel.registerKeyboardAction(actionListener, stroke, JComponent.WHEN_IN_FOCUSED_WINDOW);        
        
        return panel;
    }

    /**
     * Creates a non-editable widget to display the error message.
     *
     */
    final JComponent createErrorMessage(Throwable t)
    {
        String txt = t.getLocalizedMessage();
        JEditorPane msg = new JEditorPane();
        msg.setContentType("text/plain");
        msg.setEditable(false);
        msg.setText(txt);
        return msg;
    }

    /**
     * Creates a non-editable widget to display the detailed stack trace.
     */
    JScrollPane createDetailedMessage()
    {
        stacktrace = new JTextPane();
        stacktrace.setEditable(false);
        return new JScrollPane(stacktrace,
                ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
                ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
    }

    /**
     * Recursively print the stack trace on the given buffer.
     */
    StringBuilder generateStackTrace(Throwable t, StringBuilder buffer)
    {
        buffer.append(t.getClass().getName()).append(": ").append(t.getMessage()).append(NEWLINE);
        buffer.append(toString(t.getStackTrace()));
        Throwable cause = t.getCause();
        if (cause != null && !cause.equals(t))
        {
            buffer.append("Caused by: ");
            generateStackTrace(cause, buffer);
        }
        return buffer;
    }

    StringBuilder toString(StackTraceElement[] traces)
    {
        StringBuilder err = new StringBuilder();
        for (StackTraceElement e : traces)
        {
            if (!isFiltering || !isSuppressed(e.getClassName()))
            {
                String str = e.toString();
                err.append(INDENT).append(str).append(NEWLINE);
            }
        }
        return err;
    }

    /**
     * Affirms if the error messages from the given class name is to be suppressed.
     */
    private boolean isSuppressed(String className)
    {
        return FILTERS.stream().anyMatch(className::startsWith);
    }
}
