blob: 218467f8ffb3585b999a1df2dd0184923372f999 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.ignite.internal.util;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import org.apache.ignite.lang.IgniteInternalCheckedException;
import org.apache.ignite.lang.IgniteInternalException;
import org.apache.ignite.lang.IgniteTetraFunction;
import org.apache.ignite.lang.IgniteTriFunction;
import org.jetbrains.annotations.Nullable;
/**
* Exception utils.
*/
public final class ExceptionUtils {
/**
* The names of methods commonly used to access a wrapped exception.
*/
private static final String[] CAUSE_METHOD_NAMES = new String[]{
"getCause",
"getNextException",
"getTargetException",
"getException",
"getSourceException",
"getRootCause",
"getCausedByException",
"getNested",
"getLinkedException",
"getNestedException",
"getLinkedCause",
"getThrowable"
};
/**
* The Method object for Java 1.4 getCause.
*/
private static final Method THROWABLE_CAUSE_METHOD;
static {
Method causeMtd;
try {
causeMtd = Throwable.class.getMethod("getCause", null);
} catch (Exception ignored) {
causeMtd = null;
}
THROWABLE_CAUSE_METHOD = causeMtd;
}
/**
* Introspects the {@code Throwable} to obtain the cause.
*
* @param throwable The exception to examine.
* @return The wrapped exception, or {@code null} if not found.
*/
private static Throwable getCauseUsingWellKnownTypes(Throwable throwable) {
if (throwable instanceof SQLException) {
return ((SQLException) throwable).getNextException();
}
if (throwable instanceof InvocationTargetException) {
return ((InvocationTargetException) throwable).getTargetException();
}
return null;
}
/**
* Finds a {@code Throwable} by method name.
*
* @param throwable The exception to examine.
* @param mtdName The name of the method to find and invoke.
* @return The wrapped exception, or {@code null} if not found.
*/
private static Throwable getCauseUsingMethodName(Throwable throwable, String mtdName) {
Method mtd = null;
try {
mtd = throwable.getClass().getMethod(mtdName, null);
} catch (NoSuchMethodException | SecurityException ignored) {
// exception ignored
}
if (mtd != null && Throwable.class.isAssignableFrom(mtd.getReturnType())) {
try {
return (Throwable) mtd.invoke(throwable, ArrayUtils.OBJECT_EMPTY_ARRAY);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ignored) {
// exception ignored
}
}
return null;
}
/**
* Finds a {@code Throwable} by field name.
*
* @param throwable The exception to examine.
* @param fieldName The name of the attribute to examine.
* @return The wrapped exception, or {@code null} if not found.
*/
private static Throwable getCauseUsingFieldName(Throwable throwable, String fieldName) {
Field field = null;
try {
field = throwable.getClass().getField(fieldName);
} catch (NoSuchFieldException | SecurityException ignored) {
// exception ignored
}
if (field != null && Throwable.class.isAssignableFrom(field.getType())) {
try {
return (Throwable) field.get(throwable);
} catch (IllegalAccessException | IllegalArgumentException ignored) {
// exception ignored
}
}
return null;
}
/**
* Checks if the Throwable class has a {@code getCause} method.
*
* @return True if Throwable is nestable.
*/
public static boolean isThrowableNested() {
return THROWABLE_CAUSE_METHOD != null;
}
/**
* Checks whether this {@code Throwable} class can store a cause.
*
* @param throwable The {@code Throwable} to examine, may be null.
* @return Boolean {@code true} if nested otherwise {@code false}.
*/
public static boolean isNestedThrowable(Throwable throwable) {
if (throwable == null) {
return false;
}
if (throwable instanceof SQLException || throwable instanceof InvocationTargetException) {
return true;
}
if (isThrowableNested()) {
return true;
}
Class<?> cls = throwable.getClass();
for (String methodName : CAUSE_METHOD_NAMES) {
try {
Method mtd = cls.getMethod(methodName, null);
if (mtd != null && Throwable.class.isAssignableFrom(mtd.getReturnType())) {
return true;
}
} catch (NoSuchMethodException | SecurityException ignored) {
// exception ignored
}
}
try {
Field field = cls.getField("detail");
if (field != null) {
return true;
}
} catch (NoSuchFieldException | SecurityException ignored) {
// exception ignored
}
return false;
}
/**
* Introspects the {@code Throwable} to obtain the cause.
*
* @param throwable The throwable to introspect for a cause, may be null.
* @return The cause of the {@code Throwable}, {@code null} if none found or null throwable input.
*/
public static Throwable getCause(Throwable throwable) {
return getCause(throwable, CAUSE_METHOD_NAMES);
}
/**
* Introspects the {@code Throwable} to obtain the cause.
*
* @param throwable The throwable to introspect for a cause, may be null.
* @param mtdNames The method names, null treated as default set.
* @return The cause of the {@code Throwable}, {@code null} if none found or null throwable input.
*/
public static Throwable getCause(Throwable throwable, String[] mtdNames) {
if (throwable == null) {
return null;
}
Throwable cause = getCauseUsingWellKnownTypes(throwable);
if (cause == null) {
if (mtdNames == null) {
mtdNames = CAUSE_METHOD_NAMES;
}
for (String mtdName : mtdNames) {
if (mtdName != null) {
cause = getCauseUsingMethodName(throwable, mtdName);
if (cause != null) {
break;
}
}
}
if (cause == null) {
cause = getCauseUsingFieldName(throwable, "detail");
}
}
return cause;
}
/**
* Returns the list of {@code Throwable} objects in the exception chain.
*
* <p>A throwable without cause will return a list containing one element - the input throwable. A throwable with one cause
* will return a list containing two elements - the input throwable and the cause throwable.
* A {@code null} throwable will return a list of size zero.
*
* <p>This method handles recursive cause structures that might otherwise cause infinite loops. The cause chain is processed until
* the end is reached, or until the next item in the chain is already in the result set.
*
* @param throwable The throwable to inspect, may be null.
* @return The list of throwables, never null.
*/
public static List<Throwable> getThrowableList(Throwable throwable) {
List<Throwable> list = new ArrayList<>();
while (throwable != null && !list.contains(throwable)) {
list.add(throwable);
throwable = getCause(throwable);
}
return list;
}
/**
* Collects suppressed exceptions from throwable and all it causes.
*
* @param t Throwable.
* @return List of suppressed throwables.
*/
public static List<Throwable> getSuppressedList(@Nullable Throwable t) {
List<Throwable> result = new ArrayList<>();
if (t == null) {
return result;
}
do {
for (Throwable suppressed : t.getSuppressed()) {
result.add(suppressed);
result.addAll(getSuppressedList(suppressed));
}
} while ((t = t.getCause()) != null);
return result;
}
/**
* A way to get the entire nested stack-trace of an throwable.
*
* @param throwable The {@code Throwable} to be examined.
* @return The nested stack trace, with the root cause first.
*/
public static String getFullStackTrace(Throwable throwable) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw, true);
var ts = getThrowableList(throwable);
for (Throwable t : ts) {
t.printStackTrace(pw);
if (isNestedThrowable(t)) {
break;
}
}
return sw.getBuffer().toString();
}
/**
* Unwraps exception cause from wrappers like CompletionException and ExecutionException.
*
* @param e Throwable.
* @return Unwrapped throwable.
*/
public static Throwable unwrapCause(Throwable e) {
while ((e instanceof CompletionException || e instanceof ExecutionException) && e.getCause() != null) {
e = e.getCause();
}
return e;
}
/**
* Creates a new exception, which type is defined by the provided {@code supplier}, with the specified {@code t} as a cause.
* In the case when the provided cause {@code t} is an instance of {@link IgniteInternalException}
* or {@link IgniteInternalCheckedException}, the original trace identifier and full error code are preserved.
* Otherwise, a newly generated trace identifier and {@code defaultCode} are used.
*
* @param supplier Reference to a exception constructor.
* @param defaultCode Error code to be used in the case when the provided cause {@code t} is not an instance of Ignite exception.
* @param t Cause to be used.
* @param <T> Type of a new exception.
* @return New exception with the given cause.
*/
public static <T extends Exception> T withCause(IgniteTriFunction<UUID, Integer, Throwable, T> supplier, int defaultCode, Throwable t) {
return withCauseInternal((traceId, code, message, cause) -> supplier.apply(traceId, code, t), defaultCode, t);
}
/**
* Creates a new exception, which type is defined by the provided {@code supplier}, with the specified {@code t} as a cause.
* In the case when the provided cause {@code t} is an instance of {@link IgniteInternalException}
* or {@link IgniteInternalCheckedException}, the original trace identifier and full error code are preserved.
* Otherwise, a newly generated trace identifier and {@code defaultCode} are used.
*
* @param supplier Reference to a exception constructor.
* @param defaultCode Error code to be used in the case when the provided cause {@code t} is not an instance of Ignite exception.
* @param message Detailed error message.
* @param t Cause to be used.
* @param <T> Type of a new exception.
* @return New exception with the given cause.
*/
public static <T extends Exception> T withCause(
IgniteTetraFunction<UUID, Integer, String, Throwable, T> supplier,
int defaultCode,
String message,
Throwable t
) {
return withCauseInternal((traceId, code, m, cause) -> supplier.apply(traceId, code, message, t), defaultCode, t);
}
/**
* Creates a new exception, which type is defined by the provided {@code supplier}, with the specified {@code t} as a cause
* and full error code {@code code}.
* In the case when the provided cause {@code t} is an instance of {@link IgniteInternalException}
* or {@link IgniteInternalCheckedException}, the original trace identifier preserved.
* Otherwise, a newly generated trace identifier is used.
*
* @param supplier Reference to a exception constructor.
* @param code New error code.
* @param t Cause to be used.
* @param <T> Type of a new exception.
* @return New exception with the given cause.
*/
public static <T extends Exception> T withCauseAndCode(IgniteTriFunction<UUID, Integer, Throwable, T> supplier, int code, Throwable t) {
return withCauseInternal((traceId, c, message, cause) -> supplier.apply(traceId, code, t), code, t);
}
/**
* Creates a new exception, which type is defined by the provided {@code supplier}, with the specified {@code t} as a cause,
* full error code {@code code} and error message {@code message}.
* In the case when the provided cause {@code t} is an instance of {@link IgniteInternalException}
* or {@link IgniteInternalCheckedException}, the original trace identifier preserved.
* Otherwise, a newly generated trace identifier is used.
*
* @param supplier Reference to a exception constructor.
* @param code New error code.
* @param message Detailed error message.
* @param t Cause to be used.
* @param <T> Type of a new exception.
* @return New exception with the given cause.
*/
public static <T extends Exception> T withCauseAndCode(
IgniteTetraFunction<UUID, Integer, String, Throwable, T> supplier,
int code,
String message,
Throwable t
) {
return withCauseInternal((traceId, c, m, cause) -> supplier.apply(traceId, code, message, t), code, t);
}
/**
* Extracts the trace identifier and full error code from ignite exception and creates a new one based on the provided {@code supplier}.
*
* @param supplier Supplier to create a concrete exception instance.
* @param defaultCode Error code to be used in the case when the provided cause {@code t} is not an instance of Ignite exception.
* @param t Cause.
* @param <T> Type of a new exception.
* @return New
*/
private static <T extends Exception> T withCauseInternal(
IgniteTetraFunction<UUID, Integer, String, Throwable, T> supplier,
int defaultCode,
Throwable t
) {
Throwable unwrapped = unwrapCause(t);
if (unwrapped instanceof IgniteInternalException) {
IgniteInternalException iie = (IgniteInternalException) unwrapped;
return supplier.apply(iie.traceId(), iie.code(), iie.getMessage(), t);
} else if (unwrapped instanceof IgniteInternalCheckedException) {
IgniteInternalCheckedException iice = (IgniteInternalCheckedException) unwrapped;
return supplier.apply(iice.traceId(), iice.code(), iice.getMessage(), t);
}
return supplier.apply(UUID.randomUUID(), defaultCode, t.getMessage(), t);
}
}