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

com.numdata.oss.StackTools Maven / Gradle / Ivy

There is a newer version: 1.22
Show newest version
/*
 * Copyright (c) 2017, Numdata BV, The Netherlands.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Numdata nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL NUMDATA BV BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.numdata.oss;

import java.io.*;
import javax.swing.text.html.*;

import org.jetbrains.annotations.*;

/**
 * This class defines utility methods to work on stack traces.
 *
 * @author Peter S. Heijnen
 */
public class StackTools
{
	/**
	 * Utility class is not supposed to be instantiated.
	 */
	private StackTools()
	{
	}

	/**
	 * Append stack trace from {@link Throwable} to a {@link StringBuffer}. The
	 * stack trace can be filtered using the {@code stopTraceAt} and {@code
	 * excludeJavaAPI} arguments. If {@code throwable} is {@code null}, this
	 * method has no effect.
	 *
	 * @param sb             Buffer to append stack trace to.
	 * @param throwable      Throwable object to get stack trace from.
	 * @param stopTraceAt    Stop stack trace when this class is encountered
	 *                       ({@code null} to always show whole stack).
	 * @param excludeJavaAPI Exclude Java API classes from stack trace.
	 */
	public static void appendStackTrace( @NotNull final StringBuilder sb, @Nullable final Throwable throwable, @Nullable final Class stopTraceAt, final boolean excludeJavaAPI )
	{
		if ( throwable != null )
		{
			sb.append( throwable );
			sb.append( '\n' );

			final StackTraceElement[] trace = createTrace( throwable, 0, stopTraceAt, excludeJavaAPI );
			for ( final StackTraceElement element : trace )
			{
				sb.append( "\tat " );
				sb.append( element );
				sb.append( '\n' );
			}

			StackTraceElement[] effectTrace = trace;
			for ( Throwable cause = throwable.getCause(); cause != null; cause = cause.getCause() )
			{
				sb.append( "Caused by: " );
				sb.append( cause );
				sb.append( '\n' );

				final StackTraceElement[] causeTrace = createTrace( cause, 0, stopTraceAt, excludeJavaAPI );

				final int commonFrames = getCommonFrameCount( causeTrace, effectTrace );

				for ( int i = 0; i < causeTrace.length - commonFrames; i++ )
				{
					sb.append( "\tat " );
					sb.append( causeTrace[ i ] );
					sb.append( '\n' );
				}

				if ( commonFrames != 0 )
				{
					sb.append( "\t... " );
					sb.append( commonFrames );
					sb.append( " more" );
					sb.append( '\n' );
				}

				effectTrace = causeTrace;
			}
		}
	}

	/**
	 * Create a new stack trace for the specified {@link Throwable} object. The
	 * stack trace can be filtered using the {@code stopTraceAt} and {@code
	 * excludeJavaAPI} arguments.
	 *
	 * @param throwable      Throwable to get stack trace from.
	 * @param startIndex     Stack element index to start trace at.
	 * @param stopTraceAt    Stop stack trace when this class is encountered
	 *                       ({@code null} to always show whole stack).
	 * @param excludeJavaAPI Exclude Java API classes from stack trace.
	 *
	 * @return Stack trace (first element is the most recent entry).
	 */
	public static StackTraceElement[] createTrace( final Throwable throwable, final int startIndex, @Nullable final Class stopTraceAt, final boolean excludeJavaAPI )
	{
		final StackTraceElement[] result;

		final StackTraceElement[] fullTrace = throwable.getStackTrace();

		int endIndex = fullTrace.length;
		if ( excludeJavaAPI )
		{
			while ( endIndex > startIndex )
			{
				final String className = fullTrace[ --endIndex ].getClassName();

				if ( !className.startsWith( "java." )
				     && !className.startsWith( "javax.swing." )
				     && !className.startsWith( "sun." ) )
				{
					endIndex++;
					break;
				}
			}
		}

		if ( stopTraceAt != null )
		{
			final String stopAtName = stopTraceAt.getName();

			for ( int i = endIndex; --i >= startIndex; )
			{
				final String className = fullTrace[ i ].getClassName();

				if ( stopAtName.equals( className ) )
				{
					endIndex = i + 1;
					break;
				}
			}
		}

		if ( ( startIndex == 0 ) && ( endIndex == fullTrace.length ) )
		{
			result = fullTrace;
		}
		else
		{
			result = new StackTraceElement[ endIndex - startIndex ];
			System.arraycopy( fullTrace, startIndex, result, 0, endIndex - startIndex );
		}

		return result;
	}

	/**
	 * Get number of frames in the specified stack trace that are the same as
	 * another stack trace. The trace is compared back to front.
	 *
	 * @param trace Stack trace to analyse.
	 * @param other Stack trace to compare with.
	 *
	 * @return Number of trailing frames in {@code trace} that are the same as
	 * {@code other}; {@code 0} if no frames are the same.
	 */
	private static int getCommonFrameCount( final StackTraceElement[] trace, final StackTraceElement[] other )
	{
		final int framesInCommon;
		{
			int m = trace.length - 1;
			int n = other.length - 1;

			while ( ( m >= 0 ) && ( n >= 0 ) && trace[ m ].equals( other[ n ] ) )
			{
				m--;
				n--;
			}

			framesInCommon = trace.length - 1 - m;
		}
		return framesInCommon;
	}

	/**
	 * Get short-form description for a stack trace element. This simply takes
	 * the output of {@link StackTraceElement#toString()}, and removed the
	 * package name from it.
	 *
	 * @param element Stack trace element to describe.
	 *
	 * @return Short-form description.
	 */
	public static String getShortDescription( final StackTraceElement element )
	{
		final String className = element.getClassName();
		final String toString = element.toString();

		return toString.substring( className.lastIndexOf( (int)'.' ) );
	}

	/**
	 * Get stack trace from {@link Throwable} object as string. The stack trace
	 * can be filtered using the {@code stopTraceAt} and {@code excludeJavaAPI}
	 * arguments.
	 *
	 * @param throwable      Throwable object to get stack trace from.
	 * @param stopTraceAt    Stop stack trace when this class is encountered
	 *                       ({@code null} to always show whole stack).
	 * @param excludeJavaAPI Exclude Java API classes from stack trace.
	 *
	 * @return Stack trace message; {@code null} if {@code throwable} is {@code
	 * null}.
	 */
	@Nullable
	public static String getStackTrace( @Nullable final Throwable throwable, @Nullable final Class stopTraceAt, final boolean excludeJavaAPI )
	{
		final String result;

		if ( throwable != null )
		{
			final StringBuilder sb = new StringBuilder();
			appendStackTrace( sb, throwable, stopTraceAt, excludeJavaAPI );
			result = sb.toString();
		}
		else
		{
			result = null;
		}

		return result;
	}

	/**
	 * Print stack trace to the specified {@link PrintStream}. The stack trace
	 * can be filtered using the {@code stopTraceAt} and {@code excludeJavaAPI}
	 * arguments.
	 *
	 * @param ps             {@link PrintStream} to print stack trace to.
	 * @param throwable      Throwable object to print stack trace of.
	 * @param stopTraceAt    Stop stack trace when this class is encountered
	 *                       ({@code null} to always show whole stack).
	 * @param excludeJavaAPI Exclude Java API classes from stack trace.
	 */
	public static void printStackTrace( @NotNull final PrintStream ps, @NotNull final Throwable throwable, @Nullable final Class stopTraceAt, final boolean excludeJavaAPI )
	{
		ps.println( throwable );

		final StackTraceElement[] trace = createTrace( throwable, 0, stopTraceAt, excludeJavaAPI );
		for ( final StackTraceElement element : trace )
		{
			ps.print( "\tat " );
			ps.println( element );
		}

		StackTraceElement[] effectTrace = trace;
		for ( Throwable cause = throwable.getCause(); cause != null; cause = cause.getCause() )
		{
			ps.print( "Caused by: " );
			ps.println( cause );

			final StackTraceElement[] causeTrace = createTrace( cause, 0, stopTraceAt, excludeJavaAPI );

			final int commonFrames = getCommonFrameCount( causeTrace, effectTrace );

			for ( int i = 0; i < causeTrace.length - commonFrames; i++ )
			{
				ps.print( "\tat " );
				ps.println( causeTrace[ i ] );
			}

			if ( commonFrames != 0 )
			{
				ps.print( "\t... " );
				ps.print( commonFrames );
				ps.println( " more" );
			}

			effectTrace = causeTrace;
		}
	}

	/**
	 * Print stack trace to the specified {@link PrintWriter}. The stack trace
	 * can be filtered using the {@code stopTraceAt} and {@code excludeJavaAPI}
	 * arguments.
	 *
	 * @param pw             {@link PrintWriter} to print stack trace to.
	 * @param throwable      Throwable object to print stack trace of.
	 * @param stopTraceAt    Stop stack trace when this class is encountered
	 *                       ({@code null} to always show whole stack).
	 * @param excludeJavaAPI Exclude Java API classes from stack trace.
	 */
	public static void printStackTrace( @NotNull final PrintWriter pw, @NotNull final Throwable throwable, @Nullable final Class stopTraceAt, final boolean excludeJavaAPI )
	{
		pw.println( throwable );

		final StackTraceElement[] trace = createTrace( throwable, 0, stopTraceAt, excludeJavaAPI );
		for ( final StackTraceElement element : trace )
		{
			pw.print( "\tat " );
			pw.println( element );
		}

		StackTraceElement[] effectTrace = trace;
		for ( Throwable cause = throwable.getCause(); cause != null; cause = cause.getCause() )
		{
			pw.print( "Caused by: " );
			pw.println( cause );

			final StackTraceElement[] causeTrace = createTrace( cause, 0, stopTraceAt, excludeJavaAPI );

			final int commonFrames = getCommonFrameCount( causeTrace, effectTrace );

			for ( int i = 0; i < causeTrace.length - commonFrames; i++ )
			{
				pw.print( "\tat " );
				pw.println( causeTrace[ i ] );
			}

			if ( commonFrames != 0 )
			{
				pw.print( "\t... " );
				pw.print( commonFrames );
				pw.println( " more" );
			}

			effectTrace = causeTrace;
		}
	}

	/**
	 * Print stack trace to {@link System#err}.
	 *
	 * @param throwable Throwable object to print stack trace of.
	 */
	public static void printStackTrace( @NotNull final Throwable throwable )
	{
		printStackTrace( System.err, throwable, null, true );
	}

	/**
	 * Prints the stack trace for the given throwable to the given appendable
	 * character sequence.
	 *
	 * @param out       Character sequence to append to.
	 * @param throwable Throwable to get the stack trace from.
	 *
	 * @throws IOException if an I/O error occurs.
	 */
	public static void printStackTraceAsHTML( @NotNull final Appendable out, @NotNull final Throwable throwable )
	throws IOException
	{
		printStackTraceAsHTML( out, throwable, null );
	}

	/**
	 * Prints the stack trace for the given throwable to the given appendable
	 * character sequence.
	 *
	 * @param out         Character sequence to append to.
	 * @param throwable   Throwable to get the stack trace from.
	 * @param stopTraceAt Stop stack trace when this class is encountered
	 *                    ({@code null} to always show whole stack).
	 *
	 * @throws IOException if an I/O error occurs.
	 */
	public static void printStackTraceAsHTML( @NotNull final Appendable out, @NotNull final Throwable throwable, @Nullable final Class stopTraceAt )
	throws IOException
	{
		StackTraceElement[] causedTrace = null;

		Throwable cause = throwable;
		do
		{
			final StackTraceElement[] trace = cause.getStackTrace();

			int m = trace.length - 1;

			//noinspection ObjectEquality
			if ( cause != throwable )
			{
				out.append( '\n' );
				out.append( "Caused by: " );
				int n = causedTrace.length - 1;

				while ( ( m >= 0 ) && ( n >= 0 ) && trace[ m ].equals( causedTrace[ n ] ) )
				{
					m--;
					n--;
				}
			}
			out.append( cause.toString() );
			out.append( '\n' );

			final int framesInCommon = trace.length - 1 - m;

			for ( int i = 0; i <= m; i++ )
			{
				final StackTraceElement element = trace[ i ];

				final String className = element.getClassName();
				if ( stopTraceAt != null && className.equals( stopTraceAt.getName() ) )
				{
					break;
				}
				final int firstDollar = className.indexOf( (int)'$' );
				final int classDot = className.lastIndexOf( (int)'.', ( firstDollar == -1 ) ? className.length() : firstDollar );

				out.append( "\tat " );
				if ( classDot > 0 )
				{
					out.append( "" );
					out.append( className.substring( 0, classDot ) );
					out.append( "." );
				}
				out.append( "" );
				out.append( className.substring( classDot + 1 ) );
				out.append( "." );
				HTMLTools.writeText( out, element.getMethodName() );
				out.append( "" );

				out.append( '(' );
				if ( element.isNativeMethod() )
				{
					out.append( "Native Method" );
				}
				else
				{
					out.append( "" );
					out.append( element.getFileName() );
					out.append( "" );
					out.append( ':' );
					out.append( "" );
					out.append( String.valueOf( element.getLineNumber() ) );
					out.append( "" );
				}
				out.append( ")\n" );
			}

			if ( framesInCommon != 0 )
			{
				out.append( "\t... " ).append( String.valueOf( framesInCommon ) ).append( " more\n" );
			}

			causedTrace = trace;
			cause = cause.getCause();
		}
		while ( cause != null );
	}

	/**
	 * Adds rules to the given style sheet to style stack traces generated by
	 * {@link #printStackTraceAsHTML}.
	 *
	 * @param styleSheet Style sheet to be updated.
	 */
	public static void addStackTraceStyles( final StyleSheet styleSheet )
	{
		styleSheet.addRule( ".method, .line { font-weight: bold; }" );
		styleSheet.addRule( ".package { color: #444444; }" );
		styleSheet.addRule( ".class, .method { color: #0000cc; }" );
		styleSheet.addRule( ".line { color: #cc0000; }" );
	}

	/**
	 * Call {@link Throwable#toString} for the given {@link Throwable} including
	 * its cause(s).
	 *
	 * @param throwable Throwable to convert te string.
	 *
	 * @return String representation of {@code throwable} including cause(s).
	 */
	public static String toStringIncludingCauses( final Throwable throwable )
	{
		final String result;

		Throwable cause = throwable.getCause();
		if ( cause == null )
		{
			result = throwable.toString();
		}
		else
		{
			final StringBuilder sb = new StringBuilder();
			sb.append( throwable );

			while ( cause != null )
			{
				sb.append( " caused by: " );
				sb.append( cause );
				cause = cause.getCause();
			}

			result = sb.toString();
		}

		return result;
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy