All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.pepsoft.worldpainter.ErrorDialog Maven / Gradle / Ivy

There is a newer version: 2.23.2
Show newest version
/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

/*
 * ErrorDialog.java
 *
 * Created on Apr 17, 2011, 8:22:55 PM
 */

package org.pepsoft.worldpainter;

import org.pepsoft.util.mdc.MDCWrappingException;
import org.pepsoft.util.mdc.MDCWrappingRuntimeException;
import org.pepsoft.worldpainter.layers.Layer;
import org.pepsoft.worldpainter.vo.AttributeKeyVO;
import org.pepsoft.worldpainter.vo.EventVO;
import org.pepsoft.worldpainter.vo.ExceptionVO;
import org.pepsoft.worldpainter.vo.UsageVO;

import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.image.ImagingOpException;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;

import static java.awt.Dialog.ModalityType.APPLICATION_MODAL;
import static java.lang.Boolean.TRUE;
import static java.util.Collections.singletonList;
import static org.pepsoft.util.AwtUtils.doOnEventThread;
import static org.pepsoft.util.GUIUtils.scaleToUI;
import static org.pepsoft.util.mdc.MDCUtils.gatherMdcContext;
import static org.pepsoft.util.swing.MessageUtils.showInfo;
import static org.pepsoft.worldpainter.Constants.*;
import static org.pepsoft.worldpainter.util.ThreadUtils.getMostRecentThreadCount;

/**
 *
 * @author pepijn
 */
@SuppressWarnings({"unused", "Convert2Lambda", "Anonymous2MethodRef"}) // Managed by NetBeans
public class ErrorDialog extends javax.swing.JDialog {
    /** Creates new form ErrorDialog */
    public ErrorDialog(Window parent) {
        super(parent, APPLICATION_MODAL);
        init(parent);
    }

    @SuppressWarnings("StringConcatenationInsideStringBufferAppend") // Readability
    public void setException(Throwable exception) {
        SortedMap mdcContextMap = new TreeMap<>(gatherMdcContext(exception));
        if ((exception instanceof MDCWrappingException) || (exception instanceof MDCWrappingRuntimeException)) {
            exception = exception.getCause();
        }

        final UUID uuid = UUID.randomUUID();
        logger.error("[" + uuid + "] " + exception.getClass().getSimpleName() + ": " + exception.getMessage(), exception);

        event = new EventVO(EVENT_KEY_EXCEPTION);
        event.addTimestamp();
        event.setAttribute(ATTRIBUTE_KEY_EXCEPTION, new ExceptionVO(exception));
        event.setAttribute(ATTRIBUTE_KEY_UUID, uuid.toString());

        final Set> exceptionTypes = new HashSet<>();
        exceptionTypes.add(exception.getClass());
        Throwable rootCause = exception;
        String ioExceptionMessage = (exception instanceof IOException) ? exception.getMessage() : null;
        // TODO remove this temporary measure to get to the bottom of something
        boolean forceEmail = forceEmail(exception);
        while (rootCause.getCause() != null) {
            rootCause = rootCause.getCause();
            exceptionTypes.add(rootCause.getClass());
            if (rootCause instanceof IOException) {
                ioExceptionMessage = rootCause.getMessage();
            }
            if (forceEmail(rootCause)) {
                forceEmail = true;
            }
        }
        final boolean ioException = ioExceptionMessage != null;

        if (exceptionTypes.contains(OutOfMemoryError.class)) {
            setTitle("Out of Memory");
            final Integer threadCount = getMostRecentThreadCount();
            if ((threadCount != null) && (threadCount > 1)) {
                jTextArea1.setText("Not enough memory available for that operation!\n\n" +
                    "If this happened during an Export or Merge operation,\n" +
                    "try reducing the maximum thread count to " + (threadCount - 1) + " on the\n" +
                    "Performance tab of the Preferences screen.");
            } else {
                jTextArea1.setText("Not enough memory available for that operation!\n\n"
                    + "WorldPainter is already using the recommended maximum\n"
                    + "amount of memory, so it is not recommended to give it\n"
                    + "more. To be able to perform the operation you should\n"
                    + "install more memory (and reinstall WorldPainter).");
            }
            jButton1.setEnabled(false);
            jButton1.setToolTipText("Not necessary to send details of out of memory errors");
            jButton3.setEnabled(false);
        } else {
            String message = (ioExceptionMessage != null) ? ioExceptionMessage : rootCause.getMessage();
            if ((message != null) && (message.length() > 250)) {
                message = message.substring(0, 247) + "...";
            }
            final String requestedActionLine;
            if ((Main.privateContext != null) && (! forceEmail)) {
                // We can submit the error
                final Configuration config = Configuration.getInstance();
                if ((config != null) && TRUE.equals(config.getPingAllowed()) && (! ioException)) {
                    // Automatic submission is allowed; submit it automatically
                    mode = Mode.SEND_AUTOMATICALLY;
                    requestedActionLine = "The details of this error are being automatically submitted to the creator of this program.";
                } else {
                    mode = Mode.SEND_MANUALLY;
                    if (ioException) {
                        requestedActionLine = "If you think this is a bug then please use the Send Report button below to send the details of this error to the creator of this program.";
                    } else {
                        requestedActionLine = "Please help debug the problem by using the Send Report button below to send the details of this error to the creator of this program.";
                    }
                }
            } else {
                jButton1.setText("Email Details...");
                if ((! Desktop.isDesktopSupported()) || (! Desktop.getDesktop().isSupported(Desktop.Action.MAIL))) {
                    jButton1.setToolTipText("Emailing not supported on this system; please use the \"copy to clipboard\" button and mail the details to [email protected].");
                } else {
                    jButton1.setEnabled(true);
                }
                mode = Mode.REPORTING_DISABLED;
                if (ioException) {
                    requestedActionLine = "If you think this is a bug then please use the button below to email the details of this error to the creator of this program.";
                } else {
                    requestedActionLine = "Please help debug the problem by using the button below to email the details of this error to the creator of this program.";
                }
            }
            final String text;
            if (ioException) {
                text = "A read or write error has occurred.\n\n"
                    + "Message: " + message + "\n\n"
                    + requestedActionLine;
            } else {
                text = "An unexpected error has occurred.\n\n"
                    + "Type: " + rootCause.getClass().getName() + "\n"
                    + "Message: " + message + "\n\n"
                    + requestedActionLine + "\n\n"
                    + "The program may now be in an unstable state. It is recommended to restart it as soon as possible.";
            }
            jTextArea1.setText(text);
        }
        pack();

        StringBuilder sb = new StringBuilder();
        String eol = System.getProperty("line.separator");
        sb.append(exception.getClass().getName()).append(": ").append(exception.getMessage()).append(eol);
        StackTraceElement[] stackTrace = exception.getStackTrace();
        for (int i = 0; i < Math.min(stackTrace.length, 10); i++) {
            sb.append("\tat " + stackTrace[i].getClassName() + '.' + stackTrace[i].getMethodName() + '(' + stackTrace[i].getFileName() + ':' + stackTrace[i].getLineNumber() + ')' + eol);
        }
        sb.append(eol);
        if (rootCause != exception) {
            sb.append("Root cause:" + eol);
            sb.append(rootCause.getClass().getName() + ": " + rootCause.getMessage() + eol);
            stackTrace = rootCause.getStackTrace();
            for (int i = 0; i < Math.min(stackTrace.length, 5); i++) {
                sb.append("\tat " + stackTrace[i].getClassName() + '.' + stackTrace[i].getMethodName() + '(' + stackTrace[i].getFileName() + ':' + stackTrace[i].getLineNumber() + ')' + eol);
            }
            sb.append(eol);
        }

        if (! mdcContextMap.isEmpty()) {
            sb.append("Diagnostic context:" + eol);
            mdcContextMap.forEach((key, value) -> sb.append("\t" + key + ": " + value + eol));
            sb.append(eol);

            mdcContextMap.forEach((key, value) -> event.setAttribute(new AttributeKeyVO<>(ATTRIBUTE_KEY_MDC_ENTRY + '.' + key), value));
        }

        sb.append("WorldPainter version: " + Version.VERSION + " (" + Version.BUILD + ")" + eol);
        event.setAttribute(ATTRIBUTE_KEY_VERSION, Version.VERSION);
        event.setAttribute(ATTRIBUTE_KEY_BUILD, Version.BUILD);
        sb.append(eol);
        for (String propertyName: SYSTEM_PROPERTIES) {
            sb.append(propertyName + ": " + System.getProperty(propertyName) + eol);
            event.setAttribute(new AttributeKeyVO<>(ATTRIBUTE_KEY_SYSTEM_PROPERTY + '.' + propertyName), System.getProperty(propertyName));
        }
        sb.append(eol);
        Runtime runtime = Runtime.getRuntime();
        sb.append("Free memory: " + runtime.freeMemory() + " bytes" + eol);
        sb.append("Total memory size: " + runtime.totalMemory() + " bytes" + eol);
        sb.append("Max memory size: " + runtime.maxMemory() + " bytes" + eol);
        event.setAttribute(ATTRIBUTE_KEY_FREE_MEMORY, runtime.freeMemory());
        event.setAttribute(ATTRIBUTE_KEY_TOTAL_MEMORY, runtime.totalMemory());
        event.setAttribute(ATTRIBUTE_KEY_MAX_MEMORY, runtime.maxMemory());

        // The app may be in an unstable state, so if an exception occurs while
        // interrogating it, swallow it to prevent endless loops
        try {
            App app = App.getInstanceIfExists();
            World2 world = (app != null) ? app.getWorld() : null;
            Dimension dimension = (app != null) ? app.getDimension() : null;
            if ((world != null) && (dimension != null)) {
                sb.append(eol);
                sb.append("World name: " + world.getName() + eol);
                sb.append("Platform: " + world.getPlatform().displayName + " (" + world.getPlatform().id + ')' + eol);
                sb.append("Seed: " + dimension.getSeed() + eol);
                sb.append("Bounds: " + dimension.getLowestX() + ", " + dimension.getLowestY() + " => " + dimension.getHighestX() + ", " + dimension.getHighestY() + eol);
                sb.append("Height: " + world.getMaxHeight() + eol);
                sb.append("Number of tiles: " + dimension.getTileCount() + eol);
                Set layers = dimension.getAllLayers(false);
                sb.append("Layers in use: ");
                boolean first = true;
                for (Layer layer : layers) {
                    if (first) {
                        first = false;
                    } else {
                        sb.append(", ");
                    }
                    sb.append(layer.getName());
                }
                sb.append(eol);
                sb.append("Border: " + dimension.getBorder() + " @ " + dimension.getBorderLevel() + eol);
                sb.append("Sub surface material: " + dimension.getSubsurfaceMaterial() + eol);
                TileFactory tileFactory = dimension.getTileFactory();
                if (tileFactory instanceof HeightMapTileFactory) {
                    HeightMapTileFactory heightMapTileFactory = (HeightMapTileFactory) tileFactory;
                    sb.append("Water height: " + heightMapTileFactory.getWaterHeight() + eol);
                }
                if (world.getImportedFrom() != null) {
                    sb.append("World imported from " + world.getImportedFrom() + eol);
                }
                if (!world.isAllowMerging()) {
                    sb.append("World created in old coordinate system" + eol);
                }
            }
            if (app != null) {
                sb.append(eol);
                sb.append("Operation: " + app.getActiveOperation() + eol);
                sb.append("Radius: " + app.getRadius() + eol);
                //        sb.append("Brush shape: " + app.getBrushShape() + "/" + app.getToolBrushShape() + eol);
                sb.append("Brush: " + app.getBrush() + "/" + app.getToolBrush() + eol);
                sb.append("Level: " + app.getLevel() + "/" + app.getToolLevel() + eol);
                sb.append("Zoom: " + app.getZoom() + eol);
                sb.append("Hidden layers: " + app.getHiddenLayers());
            }
        } catch (Throwable t) {
            logger.error("Secondary exception occurred while interrogating app for exception report", t);
        }

        body = sb.toString();

        if (! "true".equals(System.getProperty("org.pepsoft.worldpainter.devMode"))) {
            logger.error(body);
        }

        if (mode == Mode.SEND_AUTOMATICALLY) {
            submitInBackground();
        }
    }

    private void init(Window parent) {
        initComponents();

        getRootPane().setDefaultButton(jButton2);

        ActionMap actionMap = rootPane.getActionMap();
        actionMap.put("cancel", new AbstractAction("cancel") {
            public void actionPerformed(ActionEvent e) {
                dispose();
            }
            
            private static final long serialVersionUID = 1L;
        });

        InputMap inputMap = rootPane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "cancel");

        scaleToUI(this);
        setLocationRelativeTo(parent);
    }

    private void close() {
        dispose();
    }

    private void email() {
        try {
            URI uri = new URI("mailto", "[email protected]?subject=WorldPainter error report&body=" + body, null);
            Desktop desktop = Desktop.getDesktop();
            desktop.mail(uri);
            showInfo(this, "A new email message should have been opened now for you to send.\nIf it did not work, please use the \"copy to clipboard\" button\nand manually mail the information to [email protected].", "Email Created");
        } catch (URISyntaxException e) {
            logger.error("URI syntax error while trying to send email", e);
            JOptionPane.showMessageDialog(this, "Could not create email message with error details!\nPlease use the \"copy to clipboard\" button and mail\nthe information to [email protected].", "Could Not Create Email", JOptionPane.ERROR_MESSAGE);
        } catch (IOException e) {
            logger.error("I/O error while trying to send email", e);
            JOptionPane.showMessageDialog(this, "Could not create email message with error details!\nPlease use the \"copy to clipboard\" button and mail\nthe information to [email protected].", "Could Not Create Email", JOptionPane.ERROR_MESSAGE);
        }
    }
    
    private void copyToClipboard() {
        Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
        StringSelection data = new StringSelection(body);
        clipboard.setContents(data, data);
        showInfo(this, "The information has been copied to the clipboard. Please paste\nit in a new email and send it to [email protected].", "Information Copied");
    }

    private void submitInBackground() {
        jButton1.setText("Sending...");
        jButton1.setEnabled(false);
        jButton2.setEnabled(false);
        new Thread("Exception Submitter") {
            @Override
            public void run() {
                try {
                    UsageVO usageVO = new UsageVO();
                    usageVO.setEvents(singletonList(event));
                    final Configuration config = Configuration.getInstance();
                    usageVO.setLaunchCount(config.getLaunchCount());
                    usageVO.setInstall(config.getUuid());
                    usageVO.setWPVersion(Version.VERSION);
                    Main.privateContext.submitUsageData(usageVO, true);
                    doOnEventThread(() -> {
                        jButton1.setText("Report Sent");
                        jButton2.setEnabled(true);
                    });
                } catch (RuntimeException e) {
                    logger.error("{} while trying to submit exception report to server (message: {})", e.getClass().getSimpleName(), e.getMessage(), e);
                    doOnEventThread(() -> {
                        JOptionPane.showMessageDialog(ErrorDialog.this, "Submitting error report failed.\nPlease use the \"Email Details...\" button below\nto email the report.");
                        jButton1.setText("Email Details...");
                        if ((! Desktop.isDesktopSupported()) || (! Desktop.getDesktop().isSupported(Desktop.Action.MAIL))) {
                            jButton1.setToolTipText("Emailing not supported on this system; please use the \"copy to clipboard\" button and mail the details to [email protected].");
                        } else {
                            jButton1.setEnabled(true);
                        }
                        mode = Mode.REPORTING_DISABLED;
                        jButton2.setEnabled(true);
                    });
                }
            }
        }.start();
    }

    private static boolean forceEmail(Throwable exception) {
        final String message = exception.getMessage();
        return (message != null) && (message.contains("terrainRanges contains null value")
                || message.contains("terrainRanges may not contain null values")
                || message.contains("aValue (rowIndex: ")
                || message.contains("Index -4 out of bounds for length 20")
                || ((exception instanceof IllegalArgumentException) && message.equals("Comparison method violates its general contract!")) // Strange Java GUI bug in JFileChooser
                || ((exception instanceof ImagingOpException) && message.equals("Unable to transform src image")) // Observed when rotating icon image
        );
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    // //GEN-BEGIN:initComponents
    private void initComponents() {

        jButton1 = new javax.swing.JButton();
        jButton2 = new javax.swing.JButton();
        jTextArea1 = new javax.swing.JTextArea();
        jButton3 = new javax.swing.JButton();

        setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
        setTitle("Unexpected Error");

        jButton1.setText("Send Report");
        jButton1.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButton1ActionPerformed(evt);
            }
        });

        jButton2.setText("Close");
        jButton2.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButton2ActionPerformed(evt);
            }
        });

        jTextArea1.setEditable(false);
        jTextArea1.setFont(getFont());
        jTextArea1.setLineWrap(true);
        jTextArea1.setRows(10);
        jTextArea1.setWrapStyleWord(true);
        jTextArea1.setOpaque(false);

        jButton3.setText("Copy details to clipboard");
        jButton3.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButton3ActionPerformed(evt);
            }
        });

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
                    .addComponent(jTextArea1, javax.swing.GroupLayout.Alignment.LEADING)
                    .addGroup(layout.createSequentialGroup()
                        .addComponent(jButton2)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(jButton3)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(jButton1)))
                .addContainerGap())
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(jTextArea1)
                .addGap(18, 18, 18)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(jButton1)
                    .addComponent(jButton2)
                    .addComponent(jButton3))
                .addContainerGap())
        );

        pack();
    }// //GEN-END:initComponents

    private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton1ActionPerformed
        switch (mode) {
            case REPORTING_DISABLED:
                email();
                break;
            case SEND_MANUALLY:
                submitInBackground();
                break;
            default:
                throw new IllegalStateException("mode " + mode);
        }
    }//GEN-LAST:event_jButton1ActionPerformed

    private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton2ActionPerformed
        close();
    }//GEN-LAST:event_jButton2ActionPerformed

    private void jButton3ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton3ActionPerformed
        copyToClipboard();
    }//GEN-LAST:event_jButton3ActionPerformed

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JButton jButton1;
    private javax.swing.JButton jButton2;
    private javax.swing.JButton jButton3;
    private javax.swing.JTextArea jTextArea1;
    // End of variables declaration//GEN-END:variables

    private String body;
    private EventVO event;
    private Mode mode = Mode.REPORTING_UNNECESSARY;

    private static final String[] SYSTEM_PROPERTIES = {
        "java.version",
        "java.vendor",
        "java.vm.version",
        "java.vm.vendor",
        "java.vm.name",
        "os.name",
        "os.arch",
        "os.version",
        "user.home",
        "user.dir",
        "user.country",
        "user.language",
    };

    private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(ErrorDialog.class);
    private static final long serialVersionUID = 1L;

    enum Mode { REPORTING_UNNECESSARY, SEND_AUTOMATICALLY, SEND_MANUALLY, REPORTING_DISABLED }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy