blob: 314961a02ffb90b7548278c638f8781324f2767f [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.getCausalChain;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.ArrayList;
import java.util.Collection;
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.base.Predicate;
import com.google.common.base.Predicates;
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 {
private static final List<Class<? extends Throwable>> BORING_THROWABLE_SUPERTYPES = ImmutableList.<Class<? extends Throwable>>of(
ExecutionException.class, InvocationTargetException.class, PropagatedRuntimeException.class, UndeclaredThrowableException.class);
private static boolean isBoring(Throwable t) {
for (Class<? extends Throwable> type: BORING_THROWABLE_SUPERTYPES)
if (type.isInstance(t)) return true;
return false;
}
private static final Predicate<Throwable> IS_THROWABLE_BORING = new Predicate<Throwable>() {
@Override
public boolean apply(Throwable input) {
return isBoring(input);
}
};
private static List<Class<? extends Throwable>> BORING_PREFIX_THROWABLE_EXACT_TYPES = ImmutableList.<Class<? extends Throwable>>of(
IllegalStateException.class, RuntimeException.class, CompoundRuntimeException.class);
/** Returns whether this is throwable either known to be boring or to have an unhelpful type name (prefix)
* which should be suppressed. null is accepted but treated as not boring. */
public static boolean isPrefixBoring(Throwable t) {
if (t==null) return false;
if (isBoring(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;
return false;
}
private 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) {
if (throwable instanceof InterruptedException) {
throw new RuntimeInterruptedException((InterruptedException) throwable);
} else if (throwable instanceof RuntimeInterruptedException) {
Thread.currentThread().interrupt();
throw (RuntimeInterruptedException) throwable;
}
Throwables.propagateIfPossible(checkNotNull(throwable));
throw new PropagatedRuntimeException(throwable);
}
/**
* See {@link #propagate(Throwable)}. If wrapping the exception, then include the given message;
* otherwise the message is not used.
*/
public static RuntimeException propagate(String msg, Throwable throwable) {
if (throwable instanceof InterruptedException) {
throw new RuntimeInterruptedException(msg, (InterruptedException) throwable);
} else if (throwable instanceof RuntimeInterruptedException) {
Thread.currentThread().interrupt();
throw (RuntimeInterruptedException) throwable;
}
Throwables.propagateIfPossible(checkNotNull(throwable));
throw new PropagatedRuntimeException(msg, throwable);
}
/**
* 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) {
if (throwable instanceof InterruptedException) {
throw new RuntimeInterruptedException((InterruptedException) throwable);
} else if (throwable instanceof RuntimeInterruptedException) {
Thread.currentThread().interrupt();
throw (RuntimeInterruptedException) throwable;
} else if (throwable instanceof Error) {
throw (Error) throwable;
}
}
/** 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 is not of common uninteresting types
* (ie excluding ExecutionException and PropagatedRuntimeExceptions);
* or the original throwable if all are uninteresting
*/
public static Throwable getFirstInteresting(Throwable throwable) {
return Iterables.tryFind(getCausalChain(throwable), Predicates.not(IS_THROWABLE_BORING)).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());
}
/** 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());
}
private static Throwable collapse(Throwable source, boolean collapseCausalChain, boolean includeAllCausalMessages, Set<Throwable> visited) {
visited = MutableSet.copyOf(visited);
String message = "";
Throwable collapsed = source;
int collapseCount = 0;
boolean messageIsFinal = false;
// remove boring stack traces at the head
while (isBoring(collapsed) && !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 = collapsed.getMessage();
messageIsFinal = true;
} else if (Strings.isNonBlank(collapsedS)) {
collapsedS = Strings.removeAllFromEnd(collapsedS, cause.toString(), stripBoringPrefixes(cause.toString()), cause.getMessage());
collapsedS = stripBoringPrefixes(collapsedS);
if (Strings.isNonBlank(collapsedS))
message = appendSeparator(message, collapsedS);
}
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 = messagesCause.toString();
messagesCause = messagesCause.getCause();
}
if (messagesCause!=null && !messageIsFinal) {
String extraMessage = collapseText(messagesCause, includeAllCausalMessages, ImmutableSet.copyOf(visited));
message = appendSeparator(message, extraMessage);
}
if (message==null) message = "";
return new PropagatedRuntimeException(message, collapseCausalChain ? collapsed : source, true);
}
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;
}
/** removes uninteresting items from the top of the call stack (but keeps interesting messages), and throws
* @deprecated since 0.7.0 same as {@link #propagate(Throwable)} */
public static RuntimeException propagateCollapsed(Throwable source) {
throw propagate(source);
}
/** 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);
}
/** 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());
}
private static String collapseText(Throwable t, boolean includeAllCausalMessages, Set<Throwable> visited) {
if (t == null) return null;
if (visited.contains(t)) {
// IllegalStateException sometimes refers to itself; guard against stack overflows
if (Strings.isNonBlank(t.getMessage())) return t.getMessage();
else return "("+t.getClass().getName()+", recursive cause)";
}
Throwable t2 = collapse(t, true, includeAllCausalMessages, visited);
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));
return ""+t2.getClass();
}
String result = t2.toString();
if (!includeAllCausalMessages) {
return result;
}
Throwable cause = t2.getCause();
if (cause != null) {
String causeResult = collapseText(new PropagatedRuntimeException(cause));
if (result.indexOf(causeResult)>=0)
return result;
return result + "; caused by "+causeResult;
}
return result;
}
public static RuntimeException propagate(Collection<? extends Throwable> exceptions) {
throw propagate(create(exceptions));
}
public static RuntimeException propagate(String prefix, Collection<? 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(Collection<? 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, Collection<? extends Throwable> exceptions) {
if (exceptions.size()==1) {
Throwable e = exceptions.iterator().next();
if (Strings.isBlank(prefix)) return new PropagatedRuntimeException(e);
return new PropagatedRuntimeException(prefix + ": " + Exceptions.collapseText(e), e);
}
if (exceptions.isEmpty()) {
if (Strings.isBlank(prefix)) return new CompoundRuntimeException("(empty compound exception)", exceptions);
return new CompoundRuntimeException(prefix, exceptions);
}
if (Strings.isBlank(prefix)) return new CompoundRuntimeException(exceptions.size()+" errors, including: " + Exceptions.collapseText(exceptions.iterator().next()), exceptions);
return new CompoundRuntimeException(prefix+"; "+exceptions.size()+" 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.
*/
public static boolean isPrefixImportant(Throwable t) {
if (t instanceof NoClassDefFoundError) return true;
return false;
}
/** For {@link Throwable} instances where know {@link #isPrefixImportant(Throwable)},
* this returns a nice message for use as a prefix; otherwise this returns the class name.
* Callers should typically suppress the prefix if {@link #isPrefixBoring(Throwable)} is true. */
public static String getPrefixText(Throwable t) {
if (t instanceof NoClassDefFoundError) return "Type not found";
return JavaClassNames.cleanSimpleClassName(t);
}
}