| /* |
| * 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); |
| } |
| } |