blob: 34c9030dc4fc54fb607aa9ca3ff94668dd6d123c [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.brooklyn.util.exceptions;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Predicates.instanceOf;
import static com.google.common.base.Throwables.getRootCause;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import javax.annotation.Nullable;
import org.apache.brooklyn.util.collections.MutableSet;
import org.apache.brooklyn.util.javalang.JavaClassNames;
import org.apache.brooklyn.util.text.Strings;
import com.google.common.annotations.Beta;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Supplier;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
public class Exceptions {
/** {@link Throwable} types whose existence is unhelpful in a <b>message</b>. */
private static final List<Class<? extends Throwable>> ALWAYS_BORING_MESSAGE_THROWABLE_SUPERTYPES = ImmutableList.<Class<? extends Throwable>>of(
ExecutionException.class, InvocationTargetException.class, UndeclaredThrowableException.class);
/** As {@link #ALWAYS_BORING_MESSAGE_THROWABLE_SUPERTYPES} but might carry an interesting message. */
private static final List<Class<? extends Throwable>> BORING_IF_NO_MESSAGE_THROWABLE_SUPERTYPES = ImmutableList.<Class<? extends Throwable>>of(
PropagatedRuntimeException.class, RuntimeInterruptedException.class);
public static final int MAX_COLLAPSE_RECURSIVE_DEPTH = 100;
/** NB: might be useful for stack trace, e.g. {@link ExecutionException} */
private static boolean isBoringForMessage(Throwable t) {
for (Class<? extends Throwable> type: ALWAYS_BORING_MESSAGE_THROWABLE_SUPERTYPES)
if (type.isInstance(t)) return true;
if (Strings.isBlank(t.getMessage()) || (t.getCause()!=null && t.getCause()!=t &&
(t.getCause() instanceof PropagatedRuntimeException
? t.getMessage().equals(t.getCause().getMessage())
: t.getMessage().equals(t.getCause().toString())))) {
for (Class<? extends Throwable> type: BORING_IF_NO_MESSAGE_THROWABLE_SUPERTYPES)
if (type.isInstance(t)) return true;
}
return false;
}
private static final Predicate<Throwable> IS_THROWABLE_BORING_FOR_MESSAGE = new Predicate<Throwable>() {
@Override
public boolean apply(Throwable input) {
return isBoringForMessage(input);
}
};
public static final Set<Class<? extends Throwable>> BORING_PREFIX_THROWABLE_EXACT_TYPES = MutableSet.<Class<? extends Throwable>>of(
RuntimeException.class, Exception.class, Throwable.class,
IllegalStateException.class, IllegalArgumentException.class);
public static final Set<Class<? extends Throwable>> BORING_PREFIX_THROWABLE_SUPERTYPES = MutableSet.<Class<? extends Throwable>>of(
ClassCastException.class, CompoundRuntimeException.class, PropagatedRuntimeException.class);
/** Returns whether the prefix is throwable either known to be boring or to have an unhelpful type name (prefix)
* which should be suppressed in <b>messages</b>. (They may be important in stack traces.)
* <p>
* null is accepted but treated as not boring. */
public static boolean isPrefixBoring(Throwable t) {
if (t==null) return false;
if (isBoringForMessage(t))
return true;
if (t instanceof UserFacingException) return true;
for (Class<? extends Throwable> type: BORING_PREFIX_THROWABLE_EXACT_TYPES)
if (t.getClass().equals(type)) return true;
for (Class<? extends Throwable> type: BORING_PREFIX_THROWABLE_SUPERTYPES)
if (type.isInstance(t)) return true;
return false;
}
/** @deprecated never used, but kept as it might be useful */
@Deprecated
static String stripBoringPrefixes(String s) {
ArrayList<String> prefixes = Lists.newArrayListWithCapacity(2 + BORING_PREFIX_THROWABLE_EXACT_TYPES.size() * 3);
for (Class<? extends Throwable> type : BORING_PREFIX_THROWABLE_EXACT_TYPES) {
prefixes.add(type.getCanonicalName());
prefixes.add(type.getName());
prefixes.add(type.getSimpleName());
}
prefixes.add(":");
prefixes.add(" ");
String[] ps = prefixes.toArray(new String[prefixes.size()]);
return Strings.removeAllFromStart(s, ps);
}
/**
* Propagate a {@link Throwable} as a {@link RuntimeException}.
* <p>
* Like Guava {@link Throwables#propagate(Throwable)} but:
* <li> throws {@link RuntimeInterruptedException} to handle {@link InterruptedException}s; and
* <li> wraps as PropagatedRuntimeException for easier filtering
*/
public static RuntimeException propagate(Throwable throwable) {
propagateIfInterrupt(throwable);
Throwables.throwIfUnchecked(throwable);
throw new PropagatedRuntimeException(throwable);
}
/**
* Convenience for {@link #propagateAnnotateIfWrapping(String, Throwable)}.
*
* @deprecated since 0.12.0 behaviour will change (default should be to always wrap and annotate);
* for now users should review and switch to either
* {@link #propagateAnnotated(String, Throwable)} or {@link #propagateAnnotateIfWrapping(String, Throwable)} as appropriate.
*/
@Deprecated
public static RuntimeException propagate(String msg, Throwable throwable) {
return propagate(msg, throwable, false);
}
/**
* See {@link #propagate(Throwable)}.
* <p>
* The given message is included <b>only</b> if the given {@link Throwable}
* needs to be wrapped; otherwise the message is not used.
* To always include the message, use {@link #propagateAnnotated(String, Throwable)}.
*/
public static RuntimeException propagateAnnotateIfWrapping(String msg, Throwable throwable) {
return propagate(msg, throwable, false);
}
/** As {@link #propagate(String, Throwable)} but unlike earlier deprecated version
* this always re-wraps including the given message, until semantics of that method change to match this.
* See {@link #propagateAnnotateIfWrapping(String, Throwable)} if the message
* should be omitted and the given throwable preserved if it can already be propagated. */
public static RuntimeException propagateAnnotated(String msg, Throwable throwable) {
return propagate(msg, throwable, true);
}
private static RuntimeException propagate(String msg, Throwable throwable, boolean alwaysAnnotate) {
if (throwable instanceof InterruptedException) {
Thread.currentThread().interrupt();
throw new RuntimeInterruptedException(isCauseEmbedded(msg, throwable) ? msg : msg + ": " + Exceptions.collapseText(throwable), (InterruptedException) throwable);
} else if (throwable instanceof RuntimeInterruptedException) {
Thread.currentThread().interrupt();
if (alwaysAnnotate) {
throw new RuntimeInterruptedException(isCauseEmbedded(msg, throwable) ? msg : msg + ": " + Exceptions.collapseText(throwable), (RuntimeInterruptedException) throwable);
} else {
throw (RuntimeInterruptedException) throwable;
}
}
if (throwable==null) {
throw new PropagatedRuntimeException(msg, new NullPointerException("No throwable supplied."));
}
if (!alwaysAnnotate) {
Throwables.propagateIfPossible(checkNotNull(throwable));
}
throw new PropagatedRuntimeException(msg, throwable);
}
/**
* Propagate exceptions which are interrupts (be it {@link InterruptedException}
* or {@link RuntimeInterruptedException}.
*/
public static void propagateIfInterrupt(Throwable throwable) {
if (throwable!=null && Exceptions.isCausedByInterruptInThisThread(throwable)) {
reinterruptIfNecessary(throwable);
if (throwable instanceof RuntimeException) throw (RuntimeException) throwable;
throw new RuntimeInterruptedException(throwable);
}
}
static boolean reinterruptIfNecessary(Throwable throwable) {
// previously always did this (when we caught anything caused by an interruption);
// but we shouldn't if it is cross threads,
// probably the intention is only to do that if it was actually InterruptedException being thrown,
// since throwing that clears the interrupt flag
if (throwable instanceof InterruptedException) {
Thread.currentThread().interrupt();
return true;
} else {
return false;
}
}
/**
* Propagate exceptions which are fatal.
* <p>
* Propagates only those exceptions which one rarely (if ever) wants to capture,
* such as {@link InterruptedException} and {@link Error}s.
*/
public static void propagateIfFatal(Throwable throwable) {
propagateIfInterrupt(throwable);
if (throwable instanceof Error) {
throw (Error) throwable;
}
}
/**
* Indicates whether this exception is "fatal" - i.e. in normal programming, should not be
* caught but should instead be propagating so the call-stack fails. For example, an interrupt
* should cause the task to abort rather than catching and ignoring (or "handling" incorrectly).
*/
public static boolean isFatal(Throwable throwable) {
return (throwable instanceof InterruptedException)
|| (throwable instanceof RuntimeInterruptedException)
|| (throwable instanceof Error);
}
public static Predicate<Throwable> isFatalPredicate() {
return IsFatalPredicate.INSTANCE;
}
public static void handleRootCauseIsInterruption(Throwable e) {
if (isCausedByInterruptInThisThread(e)) {
reinterruptIfNecessary(e);
throw new RuntimeInterruptedException(e);
}
}
public static boolean isRootCauseIsInterruption(Throwable e) {
Throwable root = getRootCause(e);
return (root instanceof InterruptedException || root instanceof RuntimeInterruptedException);
}
public static boolean isCausedByInterruptInAnyThread(Throwable e) {
Throwable interrupt = getFirstThrowableMatching(e, t -> t instanceof InterruptedException || t instanceof RuntimeInterruptedException);
if (interrupt==null) return false;
return true;
}
public static boolean isCausedByInterruptInThisThread(Throwable e) {
Throwable interrupt = getFirstThrowableMatching(e, t -> t instanceof InterruptedException || t instanceof RuntimeInterruptedException);
if (interrupt==null) return false;
if (interrupt instanceof RuntimeInterruptedException) {
return ((RuntimeInterruptedException)interrupt).isFromCurrentThread();
}
return true;
}
public static boolean isCauseEmbedded(String message, Throwable cause) {
if (cause==null) return true;
if (message==null) return false;
String causalText = Exceptions.collapseText(cause);
if (Strings.isBlank(causalText)) return false;
return message.endsWith(causalText);
}
private static class IsFatalPredicate implements Predicate<Throwable> {
private static final IsFatalPredicate INSTANCE = new IsFatalPredicate();
@Override
public boolean apply(Throwable input) {
return input != null && isFatal(input);
}
}
/** returns the first exception of the given type, or null */
@SuppressWarnings("unchecked")
public static <T extends Throwable> T getFirstThrowableOfType(Throwable from, Class<T> clazz) {
return (T) Iterables.tryFind(getCausalChain(from), instanceOf(clazz)).orNull();
}
/** returns the first exception that matches the filter, or null */
public static Throwable getFirstThrowableMatching(Throwable from, Predicate<? super Throwable> filter) {
return Iterables.tryFind(getCausalChain(from), filter).orNull();
}
/** returns the first exception in the call chain which whose message is potentially interesting,
* in the sense that it is has some chance of giving helpful information as the cause.
* <p>
* more specifically this drops those which typically wrap such causes giving chain / thread info,
* reporting rather than causal explanation or important context --
* ie excluding {@link ExecutionException} always,
* and {@link PropagatedRuntimeException} if it has no message,
* and similar such.
* <p>
* if all are "uninteresting" in this sense (which should not normally be the case)
* this method just returns the original.
* <p>
* often looking for a {@link UserFacingException} eg using {@link #getFirstThrowableOfType(Throwable, Class)}
* is a better way to give a user-facing message.
*/
public static Throwable getFirstInteresting(Throwable throwable) {
return Iterables.tryFind(getCausalChain(throwable), Predicates.not(IS_THROWABLE_BORING_FOR_MESSAGE)).or(throwable);
}
/** creates (but does not throw) a new {@link PropagatedRuntimeException} whose
* message and cause are taken from the first _interesting_ element in the source */
public static Throwable collapse(Throwable source) {
return collapse(source, true);
}
/** as {@link #collapse(Throwable)} but includes causal messages in the message as per {@link #collapseTextIncludingAllCausalMessages(Throwable)};
* use with care (limit once) as repeated usage can result in multiple copies of the same message */
public static Throwable collapseIncludingAllCausalMessages(Throwable source) {
return collapse(source, true, true, ImmutableSet.<Throwable>of(), new Object[0]);
}
/** creates (but does not throw) a new {@link PropagatedRuntimeException} whose
* message is taken from the first _interesting_ element in the source,
* and optionally also the causal chain */
public static Throwable collapse(Throwable source, boolean collapseCausalChain) {
return collapse(source, collapseCausalChain, false, ImmutableSet.<Throwable>of(), new Object[0]);
}
/** As {@link Throwables#getCausalChain(Throwable)} but safe in the face of perverse classes which return themselves as their cause or otherwise have a recursive causal chain. */
public static List<Throwable> getCausalChain(Throwable t) {
Set<Throwable> result = MutableSet.of();
while (t!=null) {
if (!result.add(t)) break;
t = t.getCause();
}
return ImmutableList.copyOf(result);
}
private static boolean isCausalChainDepthExceeding(Throwable t, int size) {
if (size<0) return true;
if (t==null) return false;
return isCausalChainDepthExceeding(t.getCause(), size-1);
}
private static Throwable collapse(Throwable source, boolean collapseCausalChain, boolean includeAllCausalMessages, Set<Throwable> visited, Object contexts[]) {
if (visited.isEmpty()) {
if (isCausalChainDepthExceeding(source, MAX_COLLAPSE_RECURSIVE_DEPTH)) {
// do fast check above, then do deeper check which survives recursive causes
List<Throwable> chain = getCausalChain(source);
if (chain.size() > MAX_COLLAPSE_RECURSIVE_DEPTH) {
// if it's an OOME or other huge stack, shrink it so we don't spin huge cycles processing the trace and printing it
// (sometimes generating subsequent OOME's in logback that mask the first!)
// coarse heuristic for how to reduce it, but that's better than killing cpu, causing further errors, and suppressing the root cause altogether!
String msg = chain.get(0).getMessage();
if (msg!=null && msg.length() > 512) msg = msg.substring(0, 500)+"...";
return new PropagatedRuntimeException("Huge stack trace (size "+chain.size()+", removing all but last few), "
+ "starting: "+chain.get(0).getClass().getName()+": "+msg+"; ultimately caused by: ",
chain.get(chain.size() - 10));
}
}
}
visited = MutableSet.copyOf(visited);
String message = "";
Throwable collapsed = source;
int collapseCount = 0;
boolean messageIsFinal = false;
// remove boring exceptions at the head; if message is interesting append it
while ((isBoringForMessage(collapsed) || isSkippableInContext(collapsed, contexts)) && !messageIsFinal) {
collapseCount++;
Throwable cause = collapsed.getCause();
if (cause==null) {
// everything in the tree is boring...
return source;
}
if (!visited.add(collapsed)) {
// there is a recursive loop
break;
}
String collapsedS = collapsed.getMessage();
if (collapsed instanceof PropagatedRuntimeException && ((PropagatedRuntimeException)collapsed).isCauseEmbeddedInMessage()) {
message = collapsedS;
messageIsFinal = true;
}
collapsed = cause;
}
// if no messages so far (ie we will be the toString) then remove boring prefixes from the message
Throwable messagesCause = collapsed;
while (messagesCause!=null && isPrefixBoring(messagesCause) && Strings.isBlank(message)) {
collapseCount++;
if (Strings.isNonBlank(messagesCause.getMessage())) {
message = messagesCause.getMessage();
messagesCause = messagesCause.getCause();
break;
}
visited.add(messagesCause); messagesCause = messagesCause.getCause();
}
if (collapseCount==0 && !includeAllCausalMessages)
return source;
if (collapseCount==0 && messagesCause!=null) {
message = getMessageWithAppropriatePrefix(messagesCause);
messagesCause = messagesCause.getCause();
}
if (messagesCause!=null && !messageIsFinal) {
String extraMessage = collapseText(messagesCause, includeAllCausalMessages, ImmutableSet.copyOf(visited), contexts);
message = appendSeparator(message, extraMessage);
}
if (message==null) message = "";
return new PropagatedRuntimeException(message, collapseCausalChain ? collapsed : source, Strings.isNonBlank(message));
}
/** True if the given exception is skippable in any of the supplied contexts. */
public static boolean isSkippableInContext(Throwable e, Object... contexts) {
if (!(e instanceof CanSkipInContext)) return false;
for (Object c: contexts) {
if (((CanSkipInContext)e).canSkipInContext(c)) {
return true;
}
}
return false;
}
static String appendSeparator(String message, String next) {
if (Strings.isBlank(message))
return next;
if (Strings.isBlank(next))
return message;
if (message.endsWith(next))
return message;
if (message.trim().endsWith(":") || message.trim().endsWith(";"))
return message.trim()+" "+next;
return message + ": " + next;
}
/** like {@link #collapse(Throwable)} but returning a one-line message suitable for logging without traces */
public static String collapseText(Throwable t) {
return collapseText(t, false);
}
/** as {@link #collapseText(Throwable)} but skipping any throwables which implement {@link CanSkipInContext}
* and indicate they should be skipped any of the given contexts */
public static String collapseTextInContext(Throwable t, Object ...contexts) {
return collapseText(t, false, ImmutableSet.<Throwable>of(), contexts);
}
/** normally {@link #collapseText(Throwable)} will stop following causal chains when encountering an interesting exception
* with a message; this variant will continue to follow such causal chains, showing all messages.
* for use e.g. when verbose is desired in the single-line message. */
public static String collapseTextIncludingAllCausalMessages(Throwable t) {
return collapseText(t, true);
}
private static String collapseText(Throwable t, boolean includeAllCausalMessages) {
return collapseText(t, includeAllCausalMessages, ImmutableSet.<Throwable>of(), new Object[0]);
}
private static String collapseText(Throwable t, boolean includeAllCausalMessages, Set<Throwable> visited, Object contexts[]) {
if (t == null) return null;
if (visited.contains(t)) {
// If a boring-prefix class has no message it will render as multiply-visited.
// Additionally IllegalStateException sometimes refers to itself as its cause.
// In both cases, don't stack overflow!
if (Strings.isNonBlank(t.getMessage())) return t.getMessage();
if (t.getCause()!=null) return t.getCause().getClass().getName();
return t.getClass().getName();
}
Throwable t2 = collapse(t, true, includeAllCausalMessages, visited, contexts);
visited = MutableSet.copyOf(visited);
visited.add(t);
visited.add(t2);
if (t2 instanceof PropagatedRuntimeException) {
if (((PropagatedRuntimeException)t2).isCauseEmbeddedInMessage())
// normally
return t2.getMessage();
else if (t2.getCause()!=null)
return collapseText(t2.getCause(), includeAllCausalMessages, ImmutableSet.copyOf(visited), contexts);
return ""+t2.getClass();
}
String result = getMessageWithAppropriatePrefix(t2);
if (!includeAllCausalMessages) {
return result;
}
Throwable cause = t2.getCause();
if (cause != null) {
String causeResult = collapseTextInContext(new PropagatedRuntimeException(cause), contexts);
if (result.indexOf(causeResult)>=0)
return result;
return result + "; caused by "+causeResult;
}
return result;
}
public static class CollapseTextSupplier implements Supplier<String> {
final Throwable cause;
public CollapseTextSupplier(Throwable cause) { this.cause = cause; }
@Override
public String get() {
return Exceptions.collapseText(cause);
}
public Throwable getOriginal() { return cause; }
}
public static RuntimeException propagate(Iterable<? extends Throwable> exceptions) {
throw propagate(create(exceptions));
}
public static RuntimeException propagate(String prefix, Iterable<? extends Throwable> exceptions) {
throw propagate(create(prefix, exceptions));
}
/** creates the given exception, but without propagating it, for use when caller will be wrapping */
public static Throwable create(Iterable<? extends Throwable> exceptions) {
return create(null, exceptions);
}
/** creates the given exception, but without propagating it, for use when caller will be wrapping */
public static RuntimeException create(@Nullable String prefix, Iterable<? extends Throwable> exceptions) {
if (Iterables.size(exceptions)==1) {
Throwable e = exceptions.iterator().next();
if (Strings.isBlank(prefix)) return new PropagatedRuntimeException(e);
return new PropagatedRuntimeException(prefix + ": " + Exceptions.collapseText(e), e);
}
if (Iterables.isEmpty(exceptions)) {
if (Strings.isBlank(prefix)) return new CompoundRuntimeException("(empty compound exception)", exceptions);
return new CompoundRuntimeException(prefix, exceptions);
}
if (Strings.isBlank(prefix)) return new CompoundRuntimeException(Iterables.size(exceptions)+" errors, including: " + Exceptions.collapseText(exceptions.iterator().next()), exceptions);
return new CompoundRuntimeException(prefix+"; "+Iterables.size(exceptions)+" errors including: " + Exceptions.collapseText(exceptions.iterator().next()), exceptions);
}
/** Some throwables require a prefix for the message to make sense,
* for instance NoClassDefFoundError's message is often just the type.
*/
@Beta
public static boolean isPrefixRequiredForMessageToMakeSense(Throwable t) {
if (t instanceof NoClassDefFoundError) return true;
return false;
}
/** For {@link Throwable} instances where know {@link #isPrefixRequiredForMessageToMakeSense(Throwable)},
* this returns a nice message for use as a prefix;
* returns empty string if {@link #isPrefixBoring(Throwable)} is true;
* otherwise this returns the simplified class name. */
private static String getPrefixText(Throwable t) {
if (t instanceof NoClassDefFoundError) return "Invalid java type";
if (isPrefixBoring(t)) return "";
return JavaClassNames.cleanSimpleClassName(t);
}
/** Like {@link Throwable#toString()} except suppresses boring prefixes and replaces prefixes with sensible messages where required */
public static String getMessageWithAppropriatePrefix(Throwable t) {
return appendSeparator(getPrefixText(t), t.getMessage());
}
/** Returns true if the root cause is a "boring" CNF, ie a straightforward declaration that the clazz is not found;
* this permits callers to include the cause only when it is interesting, ie caused by a dependent class not loadable.
*/
public static boolean isRootBoringClassNotFound(Exception e, String clazz) {
Throwable root = Throwables.getRootCause(e);
if (!(root instanceof ClassNotFoundException)) return false;
if (clazz.equals(root.getMessage())) return true;
return false;
}
}