/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2001-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library 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
* Lesser General Public License for more details.
*/
package org.geotoolkit.util;
import java.awt.Font;
import java.awt.Component;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.Rectangle2D;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.List;
import java.util.ArrayList;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.UndeclaredThrowableException;
import org.geotoolkit.lang.Static;
import org.geotoolkit.resources.Errors;
import org.apache.sis.io.LineAppender;
/**
* Utilities methods for dealing with exceptions. Those methods can reformat the stack
* trace for console output, paint the stack trace in a {@link Graphics2D} handler, or show
* the stack trace in a <cite>Swing</cite> component.
* <p>
* The <cite>Swing</cite> component is available only if the {@code geotk-widgets-swing} module
* is available in the classpath. It looks like the following picture:
*
* <p> </p>
* <p align="center"><img src="../gui/swing/doc-files/ExceptionMonitor.png"></p>
* <p> </p>
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @author Johann Sorel (Geomatys)
* @version 3.21
*
* @see org.apache.sis.util.Exceptions
*
* @since 2.0
* @module
*/
public final class Exceptions extends Static {
/**
* Number of spaces to leave between each tab.
*/
private static final int TAB_WIDTH = 4;
/**
* Do not allow instantiation of this class.
*/
private Exceptions() {
}
/**
* Returns {@code true} if the given message is non-null and holds a reliable information.
* The message shall not be null, empty (ignoring white spaces) or equals to the string
* {@code "null"} (ignoring case).
* <p>
* This method is intended for use in assertions like below:
*
* {@preformat java
* assert Exceptions.isValidMessage(message);
* throw new SomeException(message);
* }
*
* @param message The message argument to check.
* @return {@code true} if the message is informative, or {@code false} otherwise.
*
* @since 3.21
*/
public static boolean isValidMessage(String message) {
if (message == null) {
return false;
}
message = message.trim();
return !message.isEmpty() && !message.equalsIgnoreCase("null");
}
/**
* Returns the exception trace as a string. This method get the stack trace using the
* {@link Throwable#printStackTrace(PrintWriter)} method, then replaces the tabulation
* characters by 4 white spaces.
*
* @param exception The exception to format.
* @return A string representation of the given exception.
*/
public static String formatStackTrace(final Throwable exception) {
final StringWriter writer = new StringWriter();
exception.printStackTrace(new PrintWriter(writer));
final StringBuilder buffer = new StringBuilder();
final LineAppender formatter = new LineAppender(buffer);
formatter.setTabulationWidth(TAB_WIDTH);
try {
formatter.append(writer.toString());
} catch (IOException e) {
throw new AssertionError(e);
}
return buffer.toString();
}
/**
* Writes the specified exception trace in the specified graphics context. This method is
* useful when an exception has occurred inside a {@link java.awt.Component#paint} method
* and we want to write it rather than leaving an empty window.
*
* @param graphics Graphics context in which to write exception. The graphics context shall
* be in its initial state (default affine transform, default color, <i>etc.</i>)
* @param widgetBounds Size of the trace which was being drawn.
* @param exception Exception whose trace we want to write.
*/
public static void paintStackTrace(final Graphics2D graphics,
final Rectangle widgetBounds,
final Throwable exception)
{
/*
* Obtains the exception trace in the form of a character chain.
* The carriage returns in this chain can be "\r", "\n" or "r\n".
*/
final String message = formatStackTrace(exception);
/*
* Examines the character chain line by line.
* "Glyphs" will be created as we go along and we will take advantage
* of this to calculate the necessary space.
*/
double width = 0, height = 0;
final List<GlyphVector> glyphs = new ArrayList<>();
final List<Rectangle2D> bounds = new ArrayList<>();
final int length = message.length();
final Font font = graphics.getFont();
final FontRenderContext context = graphics.getFontRenderContext();
for (int i = 0; i < length;) {
int ir = message.indexOf('\r', i);
int in = message.indexOf('\n', i);
if (ir < 0) ir = length;
if (in < 0) in = length;
final int irn = Math.min(ir, in);
final GlyphVector line = font.createGlyphVector(context, message.substring(i, irn));
final Rectangle2D rect = line.getVisualBounds();
final double w = rect.getWidth();
if (w > width) width = w;
height += rect.getHeight();
glyphs.add(line);
bounds.add(rect);
i = (Math.abs(ir - in) <= 1 ? Math.max(ir, in) : irn) + 1;
}
/*
* Proceeds to draw all the previously calculated glyphs.
*/
float xpos = widgetBounds.x + (float) (0.5 * Math.max(0, widgetBounds.width - width));
float ypos = widgetBounds.y + (float) (0.5 * Math.max(0, widgetBounds.height - height));
final int size = glyphs.size();
for (int i = 0; i < size; i++) {
final GlyphVector line = glyphs.get(i);
final Rectangle2D rect = bounds.get(i);
ypos += rect.getHeight();
graphics.drawGlyphVector(line, xpos, ypos);
}
}
/**
* Displays an error message for the specified exception. Note that this method can
* be called from any thread (not necessarily the <cite>Swing</cite> thread).
*
* @param owner Component in which the exception is produced, or {@code null} if unknown.
* @param exception Exception which has been thrown and is to be reported to the user.
* @throws UnsupportedOperationException If the {@code geotk-widgets-swing} module is
* not found on the classpath.
*
* @see org.jdesktop.swingx.JXErrorPane
* @since 3.18 (derived from 1.0)
*/
public static void show(final Component owner, final Throwable exception) throws UnsupportedOperationException {
show(new Object[] {owner, exception});
}
/**
* Displays an error message for the specified exception. Note that this method can
* be called from any thread (not necessarily the <cite>Swing</cite> thread).
*
* @param owner Component in which the exception is produced, or {@code null} if unknown.
* @param exception Exception which has been thrown and is to be reported to the user.
* @param message Message to display. If this parameter is {@code null}, then
* {@link Exception#getLocalizedMessage} will be called to obtain the message.
* @throws UnsupportedOperationException If the {@code geotk-widgets-swing} module is
* not found on the classpath.
*
* @see org.jdesktop.swingx.JXErrorPane
* @since 3.18 (derived from 1.0)
*/
public static void show(final Component owner, final Throwable exception, final String message)
throws UnsupportedOperationException
{
show(new Object[] {owner, exception, message});
}
/**
* Delegates to the {@code show} method in the {@code geotk-widgets-swing} module, if present.
*/
@SuppressWarnings("fallthrough")
private static void show(final Object[] arguments) throws UnsupportedOperationException {
final Class<?>[] types = new Class<?>[arguments.length];
switch (types.length) {
default: types[2] = String.class;
case 2: types[1] = Throwable.class;
case 1: types[0] = Component.class;
case 0: break;
}
try {
Class.forName("org.geotoolkit.internal.swing.ExceptionMonitor").getMethod("show", types).invoke(null, arguments);
} catch (ClassNotFoundException e) {
// This is the expected exception if the widget module is not available.
throw new UnsupportedOperationException(Errors.format(
Errors.Keys.MissingModule_1, "geotk-widgets-swing"), e);
} catch (NoSuchMethodException | IllegalAccessException e) {
// Should never happen, since have control on our implementation.
throw new AssertionError(e);
} catch (InvocationTargetException e) {
final Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
if (cause instanceof Error) {
throw (Error) cause;
}
// The invoked method does not declare any checked exception,
// so if we reach this point we have an unexpected exception.
throw new UndeclaredThrowableException(cause);
}
}
}