/* * BSD 3-Clause License * * Copyright (c) 2020, Taner Sener * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * 2. 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. * * 3. Neither the name of the copyright holder 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 THE COPYRIGHT HOLDER OR CONTRIBUTORS 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.arthenica.smartexception; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Set; /** *

Abstract class that includes common variables and methods for different Exceptions implementations. * * @author Taner Sener * @since 0.1.0 */ public abstract class AbstractExceptions { /** *

Default max depth used in methods which do not have a maxDepth argument. */ public static final int DEFAULT_MAX_DEPTH = 10; /** *

Default value for ignoring all causes when stack trace elements are printed or converted to string. */ public static final boolean DEFAULT_IGNORE_ALL_CAUSES = false; /** *

Stores global root package names. */ public static final Set rootPackageSet = Collections.synchronizedSet(new HashSet()); /** *

Stores global group package names. */ public static final Set groupPackageSet = Collections.synchronizedSet(new HashSet()); /** *

Stores global ignore package names. */ public static final Set ignorePackageSet = Collections.synchronizedSet(new HashSet()); /** *

Stores global ignore cause package names. */ public static final Set ignoreCausePackageSet = Collections.synchronizedSet(new HashSet()); /** *

Stores the value of global ignore all causes option. */ public static boolean ignoreAllCauses = DEFAULT_IGNORE_ALL_CAUSES; /** *

Stores the global stack trace serializer implementation. */ public static StackTraceElementSerializer stackTraceElementSerializer; /** *

Registers a new root package. * * @param packageString root package name to register */ public static void registerRootPackage(final String packageString) { rootPackageSet.add(packageString); } /** *

Clears previously registered root packages. */ public static void clearRootPackages() { rootPackageSet.clear(); } /** *

Registers a new group package. * * @param packageString group package name to register */ public static void registerGroupPackage(final String packageString) { groupPackageSet.add(packageString); } /** *

Clears previously registered group packages. */ public static void clearGroupPackages() { groupPackageSet.clear(); } /** *

Returns the global stack trace serializer implementation which is used to serialize {@link StackTraceElement} * objects in getStackTraceString methods. * * @return current stack trace serializer implementation */ public static StackTraceElementSerializer getStackTraceElementSerializer() { return stackTraceElementSerializer; } /** *

Sets the global stack trace serializer implementation which is used to serialize {@link StackTraceElement} * objects in getStackTraceString methods. * * @param stackTraceElementSerializer new stack trace serializer implementation */ public static void setStackTraceElementSerializer(final StackTraceElementSerializer stackTraceElementSerializer) { AbstractExceptions.stackTraceElementSerializer = stackTraceElementSerializer; } /** *

Registers a new ignore package. * * @param packageString ignore package name to register * @param ignoreCauseClasses ignore cause classes from this package too */ public static void registerIgnorePackage(final String packageString, final boolean ignoreCauseClasses) { ignorePackageSet.add(packageString); if (ignoreCauseClasses) { ignoreCausePackageSet.add(packageString); } } /** *

Clears previously registered ignore packages. */ public static void clearIgnorePackages() { ignorePackageSet.clear(); ignoreCausePackageSet.clear(); } /** *

Returns the value of ignore all causes option. * * @return the value of global ignore all causes option. If value is true then stack trace elements printed or * converted to string will not include causes. If value is false causes will be appended to the stack trace of * the main throwable. */ public static boolean getIgnoreAllCauses() { return ignoreAllCauses; } /** *

Sets the value of ignore all causes option. * * @param ignoreAllCauses new global ignore all causes option. If value is true then stack trace elements printed * or converted to string will not include causes. If value is false causes will be appended to the stack trace of * the main throwable. */ public static void setIgnoreAllCauses(final boolean ignoreAllCauses) { AbstractExceptions.ignoreAllCauses = ignoreAllCauses; } /** *

Returns the smart stack trace for the given throwable. * *

This method uses root packages registered by {@link #registerRootPackage(String)}, group packages registered * by {@link #registerGroupPackage(String)} and ignore packages registered by * {@link #registerIgnorePackage(String, boolean)} to build the smart stack trace. * * @param throwable parent throwable * @return a string containing the smart stack trace for the given throwable */ public static String getStackTraceString(final Throwable throwable) { return getStackTraceString(throwable, false, rootPackageSet, groupPackageSet, ignorePackageSet, 0, ignoreAllCauses); } /** *

Returns the smart stack trace for the given throwable. * *

This method uses root packages registered by {@link #registerRootPackage(String)}, group packages registered * by {@link #registerGroupPackage(String)} and ignore packages registered by * {@link #registerIgnorePackage(String, boolean)} to build the smart stack trace. * * @param throwable parent throwable * @param ignoreAllCauses ignore all causes in the exception chain * @return a string containing the smart stack trace for the given throwable */ public static String getStackTraceString(final Throwable throwable, final boolean ignoreAllCauses) { return getStackTraceString(throwable, false, rootPackageSet, groupPackageSet, ignorePackageSet, 0, ignoreAllCauses); } /** *

Returns the smart stack trace for the given throwable using packages provided. * * @param throwable parent throwable * @param rootPackageSet root packages to use for building the stack trace * @param groupPackageSet group packages to use for building the stack trace * @param ignorePackageSet ignore packages to use for building the stack trace * @return a string containing the smart stack trace for the given throwable */ public static String getStackTraceString(final Throwable throwable, final Set rootPackageSet, final Set groupPackageSet, final Set ignorePackageSet) { return getStackTraceString(throwable, false, rootPackageSet, groupPackageSet, ignorePackageSet, 0, ignoreAllCauses); } /** *

Returns the smart stack trace for the given throwable using packages provided. * * @param throwable parent throwable * @param rootPackageSet root packages to use for building the stack trace * @param groupPackageSet group packages to use for building the stack trace * @param ignorePackageSet ignore packages to use for building the stack trace * @param ignoreAllCauses ignore all causes in the exception chain * @return a string containing the smart stack trace for the given throwable */ public static String getStackTraceString(final Throwable throwable, final Set rootPackageSet, final Set groupPackageSet, final Set ignorePackageSet, final boolean ignoreAllCauses) { return getStackTraceString(throwable, false, rootPackageSet, groupPackageSet, ignorePackageSet, 0, ignoreAllCauses); } /** *

Returns the smart stack trace for the given throwable using root package provided. * * @param throwable parent throwable * @param rootPackage root package to use for building the stack trace * @return a string containing the smart stack trace for the given throwable */ public static String getStackTraceString(final Throwable throwable, final String rootPackage) { return getStackTraceString(throwable, false, Collections.singleton(rootPackage), new HashSet(), new HashSet(), 0, ignoreAllCauses); } /** *

Returns the smart stack trace for the given throwable using root package provided. * * @param throwable parent throwable * @param rootPackage root package to use for building the stack trace * @param groupPackage group package to use for building the stack trace * @return a string containing the smart stack trace for the given throwable */ public static String getStackTraceString(final Throwable throwable, final String rootPackage, final String groupPackage) { return getStackTraceString(throwable, false, Collections.singleton(rootPackage), Collections.singleton(groupPackage), new HashSet(), 0, ignoreAllCauses); } /** *

Returns the smart stack trace for the given throwable using elements found until the maxDepth. * * @param throwable parent throwable * @param maxDepth max depth in exception chain that will be used * @return a string containing the smart stack trace for the given throwable */ public static String getStackTraceString(final Throwable throwable, final int maxDepth) { return getStackTraceString(throwable, false, new HashSet(), new HashSet(), new HashSet(), maxDepth, ignoreAllCauses); } /** *

Returns the smart stack trace for the given throwable using elements found until the maxDepth. * * @param throwable parent throwable * @param maxDepth max depth in exception chain that will be used * @param ignoreAllCauses ignore all causes in the exception chain * @return a string containing the smart stack trace for the given throwable */ public static String getStackTraceString(final Throwable throwable, final int maxDepth, final boolean ignoreAllCauses) { return getStackTraceString(throwable, false, new HashSet(), new HashSet(), new HashSet(), maxDepth, ignoreAllCauses); } /** *

Returns the smart stack trace for the given throwable using parameters provided. * * @param throwable parent throwable * @param isCause throwable is a cause or not * @param rootPackageSet root packages to use for building the stack trace * @param groupPackageSet group packages to use for building the stack trace * @param ignorePackageSet ignore packages to use for building the stack trace * @param maxDepth max depth in exception chain that will be used * @param ignoreAllCauses ignore all causes in the exception chain * @return a string containing the smart stack trace for the given throwable */ public static String getStackTraceString(final Throwable throwable, final boolean isCause, final Set rootPackageSet, final Set groupPackageSet, final Set ignorePackageSet, final int maxDepth, final boolean ignoreAllCauses) { final StringBuilder builder = new StringBuilder(); if (throwable == null) { return ""; } final String className = throwable.getClass().getName(); final StackTraceElement[] stackTraceElements; if (maxDepth > 0) { stackTraceElements = getStackTrace(throwable, maxDepth); } else { stackTraceElements = getStackTrace(throwable, rootPackageSet, ignorePackageSet); } String message = throwable.getLocalizedMessage(); if (isEmpty(message)) { message = throwable.getMessage(); } /* PRINT MESSAGE */ if (isCause) { builder.append(System.lineSeparator()); builder.append("Caused by: "); builder.append(className); if (!isEmpty(message)) { builder.append(": "); builder.append(message); } } else { builder.append(System.lineSeparator()); builder.append(className); if (!isEmpty(message)) { builder.append(": "); builder.append(message); } } /* PRINT SHORT STACK TRACE */ String currentGroupPackage = null; StackTraceElement firstStackTraceElementInTheGroup = null; int currentGroupCount = 0; for (StackTraceElement traceElement : stackTraceElements) { String traceElementClassName = traceElement.getClassName(); String groupPackageMatch = getContainingPackage(traceElementClassName, groupPackageSet); if (groupPackageMatch != null) { if (!groupPackageMatch.equals(currentGroupPackage)) { appendStackTraceGroupElement(builder, currentGroupPackage, currentGroupCount, firstStackTraceElementInTheGroup); builder.append(System.lineSeparator()); builder.append("\tat "); currentGroupPackage = groupPackageMatch; firstStackTraceElementInTheGroup = traceElement; currentGroupCount = 1; } else { currentGroupCount++; } } else { currentGroupCount = appendStackTraceGroupElement(builder, currentGroupPackage, currentGroupCount, firstStackTraceElementInTheGroup); builder.append(System.lineSeparator()); builder.append("\tat "); if (stackTraceElementSerializer == null) { throw new IllegalArgumentException("Stack trace element serializer not initialized."); } else { builder.append(stackTraceElementSerializer.toString(traceElement)); } currentGroupPackage = null; } } appendStackTraceGroupElement(builder, currentGroupPackage, currentGroupCount, firstStackTraceElementInTheGroup); final Throwable cause = throwable.getCause(); if (cause != null && !containsPackage(className, ignoreCausePackageSet) && !ignoreAllCauses) { builder.append(getStackTraceString(cause, true, rootPackageSet, groupPackageSet, ignorePackageSet, maxDepth, ignoreAllCauses)); } return builder.toString(); } /** *

Appends stack trace group information for the given parameters. * * @param stringBuilder string builder that group stack trace information will be appended * @param currentGroupPackage package name of the current group * @param numberOfElementsInTheCurrentGroup number of elements in the current group * @param firstStackTraceElementInTheGroup first stack trace element of this group * @return new value for the group element count */ public static int appendStackTraceGroupElement(final StringBuilder stringBuilder, final String currentGroupPackage, final int numberOfElementsInTheCurrentGroup, final StackTraceElement firstStackTraceElementInTheGroup) { if (numberOfElementsInTheCurrentGroup > 0) { if (stackTraceElementSerializer == null) { throw new IllegalArgumentException("Stack trace element serializer not initialized."); } else { stringBuilder.append((numberOfElementsInTheCurrentGroup == 1) ? stackTraceElementSerializer.toString(firstStackTraceElementInTheGroup) : String.format("%s%s ... %d more", stackTraceElementSerializer.getModuleName(firstStackTraceElementInTheGroup), currentGroupPackage, (numberOfElementsInTheCurrentGroup - 1))); } } return 0; } /** *

Returns true if package of the given class name matches one of package names provided in * packageSet. * * @param fullClassName fully qualified class name * @param packageSet set containing package names * @return true if package of the given class name matches one of package names provided in packageSet, * false otherwise */ public static boolean containsPackage(final String fullClassName, final Set packageSet) { return getContainingPackage(fullClassName, packageSet) != null; } /** *

Checks if one of the package names in the packageSet matches the package of the given class name * and returns the matching package name. * * @param fullClassName fully qualified class name * @param packageSet set containing package names * @return one of the package names in the packageSet that matches the package of the given class name * or null if no matching package name is found in the packageSet */ public static String getContainingPackage(final String fullClassName, final Set packageSet) { for (final String parentExceptionPackage : packageSet) { if (fullClassName.startsWith(parentExceptionPackage)) { return parentExceptionPackage; } } return null; } /** *

Returns all messages found in the exception chain of the throwable as a single string. * * @param throwable parent throwable * @return a string containing all messages found in the exception chain */ public static String getAllMessages(final Throwable throwable) { final StringBuilder messageBuilder = new StringBuilder(); getAllMessages(throwable, messageBuilder); return messageBuilder.toString(); } /** *

Adds all messages found in the exception chain of the throwable into the given message builder. * * @param throwable parent throwable * @param messageBuilder message builder */ public static void getAllMessages(final Throwable throwable, final StringBuilder messageBuilder) { if (throwable != null) { final String message = throwable.getMessage(); if (!isEmpty(message)) { if (messageBuilder.length() != 0) { messageBuilder.append(System.lineSeparator()); messageBuilder.append(" - Caused by: "); } messageBuilder.append(message); } getAllMessages(throwable.getCause(), messageBuilder); } } /** *

Builds a smart stack trace for the given throwable using elements found until the maxDepth and * elements of it. * * @param throwable parent throwable * @param maxDepth max depth in exception chain that will be returned * @return an array containing stack trace elements */ public static StackTraceElement[] getStackTrace(final Throwable throwable, final int maxDepth) { final ArrayList list = new ArrayList(); if (throwable != null) { StackTraceElement[] stackTrace = throwable.getStackTrace(); for (int i = 0; (i < stackTrace.length) && (i < maxDepth); i++) { list.add(stackTrace[i]); } } return list.toArray(new StackTraceElement[0]); } /** *

Builds a smart stack trace for the given throwable using packages provided and returns elements * of it. * * @param throwable parent throwable * @param rootPackageSet root packages to use for building the stack trace * @param ignorePackageSet ignore packages to use for building the stack trace * @return an array containing stack trace elements */ public static StackTraceElement[] getStackTrace(final Throwable throwable, final Set rootPackageSet, final Set ignorePackageSet) { final ArrayList list = new ArrayList(); final ArrayList partialList = new ArrayList(); if (throwable != null) { for (StackTraceElement stackTraceElement : throwable.getStackTrace()) { final String className = stackTraceElement.getClassName(); if (!isEmpty(className)) { if (containsPackage(className, rootPackageSet)) { list.addAll(partialList); list.add(stackTraceElement); } else if (!containsPackage(className, ignorePackageSet)) { partialList.add(stackTraceElement); } } } } if (list.isEmpty()) { list.addAll(partialList); } return list.toArray(new StackTraceElement[0]); } /** *

Returns true if the given cause class is found in the exception chain of the throwable. *
*
*

Search starts from the throwable itself and goes through the exception chain up until * {@link #DEFAULT_MAX_DEPTH}. * * @param throwable parent throwable * @param causeClass class to search for * @return true if the given cause class is found in the exception chain of the throwable, false * otherwise */ public static boolean containsCause(final Throwable throwable, final Class causeClass) { return containsCause(throwable, causeClass, null); } /** *

Returns true if the given cause class and cause message is found in the exception chain of the * throwable. *
*
*

Search starts from the throwable itself and goes through the exception chain up until * {@link #DEFAULT_MAX_DEPTH}. * * @param throwable parent throwable * @param causeClass class to search for * @param causeMessage message to search for * @return true if the given cause class and cause message is found in the exception chain of the * throwable, false otherwise */ public static boolean containsCause(final Throwable throwable, final Class causeClass, final String causeMessage) { return (searchCause(throwable, causeClass, causeMessage, DEFAULT_MAX_DEPTH) != null); } /** *

Returns the cause of the throwable by walking through the exception chain. * * @param throwable parent throwable * @return the cause of the throwable found or null if throwable does not have a cause */ public static Throwable getCause(final Throwable throwable) { return getCause(throwable, DEFAULT_MAX_DEPTH); } /** *

Returns the cause of the throwable by walking through the exception chain. *
*
*

Note that this method walks through the exception chain up to given maxDepth. If exception chain * includes more items than maxDepth, cause found at maxDepth level is returned. * * @param throwable parent throwable * @param maxDepth max depth in exception chain that will be searched * @return the cause of the throwable found until maxDepth is reached in the exception * chain or null if throwable does not have a cause */ public static Throwable getCause(final Throwable throwable, final int maxDepth) { if (throwable == null) { return null; } if (maxDepth <= 0) { return throwable; } final Throwable cause = throwable.getCause(); if (cause == null) { return throwable; } else { return getCause(cause, maxDepth - 1); } } /** *

Searches for the given cause class in the exception chain of the throwable. *
*
*

Search starts from the throwable itself and goes through the exception chain up until * {@link #DEFAULT_MAX_DEPTH}. * * @param throwable parent throwable * @param causeClass class to search for * @return the throwable found or null if no class in exception chain matches the given cause class */ public static Throwable searchCause(final Throwable throwable, final Class causeClass) { return searchCause(throwable, causeClass, null, DEFAULT_MAX_DEPTH); } /** *

Searches for the given cause class and cause message in the exception chain of the throwable. *
*
*

Search starts from the throwable itself and goes through the exception chain up until * {@link #DEFAULT_MAX_DEPTH}. * * @param throwable parent throwable * @param causeClass class to search for * @param causeMessage message to search for * @return the throwable found or null if no class in exception chain matches the given cause class * and cause message */ public static Throwable searchCause(final Throwable throwable, final Class causeClass, final String causeMessage) { return searchCause(throwable, causeClass, causeMessage, DEFAULT_MAX_DEPTH); } /** *

Searches for the given cause class and cause message in the exception chain of the throwable. *
*
*

Search starts from the throwable itself and goes through the exception chain up until the given * maxDepth. * * @param throwable parent throwable * @param causeClass class to search for * @param causeMessage message to search for * @param maxDepth max depth in exception chain that will be searched * @return the throwable found or null if no class in exception chain matches the given cause class * and cause message */ public static Throwable searchCause(final Throwable throwable, final Class causeClass, final String causeMessage, final int maxDepth) { if (throwable == null) { return null; } if (isEmpty(causeMessage)) { if (throwable.getClass().equals(causeClass)) { return throwable; } } else { if (throwable.getClass().equals(causeClass) && getAllMessages(throwable).toLowerCase().contains(causeMessage.toLowerCase())) { return throwable; } } if (maxDepth <= 0) { return null; } final Throwable cause = throwable.getCause(); if (cause == null) { return null; } else { return searchCause(cause, causeClass, causeMessage, maxDepth - 1); } } /** *

Searches for the given cause class in the exception chain of the throwable. *
*
*

Search starts from the throwable itself and goes through the exception chain up until the given * maxDepth. * * @param throwable parent throwable * @param causeClass class to search for * @param maxDepth max depth in exception chain that will be searched * @return the throwable found or null if no class in exception chain matches the given cause class */ public static Throwable searchCause(final Throwable throwable, final Class causeClass, final int maxDepth) { if (throwable == null) { return null; } if (throwable.getClass().equals(causeClass)) { return throwable; } if (maxDepth <= 0) { return null; } final Throwable cause = throwable.getCause(); if (cause == null) { return null; } else { return searchCause(cause, causeClass, maxDepth - 1); } } /** *

Checks if given string is empty or not. * * @param value string to check * @return true if given string is null or has zero length after trim(), false otherwise */ public static boolean isEmpty(final String value) { if (value == null) { return true; } return value.trim().length() == 0; } /** *

Returns the package name of this class. * * @param className fully qualified class name * @return name of the package */ public static String packageName(final String className) { if (className == null) { return ""; } int index = className.lastIndexOf("."); if (index >= 0) { return className.substring(0, index); } else { return ""; } } }