ExceptionUtils.java
/*
* 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.tika.utils;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Set;
import org.apache.tika.config.ExceptionReporting;
import org.apache.tika.exception.TikaException;
import org.apache.tika.parser.ParseContext;
/**
* Turns a {@link Throwable} into the string Tika reports to callers, under the
* {@link ExceptionReporting} policy in effect. Tika's own channels record and emit exception
* text through {@link #format(Throwable, ParseContext)} so that one config setting governs
* them all; exception text produced by third-party libraries is outside its reach.
* <p>
* NOTE: If your stacktraces are truncated, make sure to start your jvm
* with: -XX:-OmitStackTraceInFastThrow
*/
public class ExceptionUtils {
private static final int MAX_CAUSE_DEPTH = 64;
private static final String TRUNCATED = "...[truncated]";
/**
* Formats {@code t} under the {@link ExceptionReporting} found in {@code context}
* (or the default policy if the context is null or has none).
*/
public static String format(Throwable t, ParseContext context) {
return format(t, ExceptionReporting.get(context));
}
/**
* Formats {@code t} for channels that have no ParseContext, such as server error
* responses and pipes crash messages.
*/
public static String format(Throwable t, ExceptionReporting reporting) {
if (reporting == null) {
//never NPE while formatting someone else's exception
reporting = new ExceptionReporting();
}
String s;
switch (reporting.getLevel()) {
case REDACTED:
s = redacted(t, false);
break;
case MESSAGE_REDACTED:
s = redacted(t, true);
break;
default:
s = full(t);
}
return truncate(s, reporting.getMaxLength());
}
/**
* @deprecated since 4.1, removal planned for 5.0; use
* {@link #format(Throwable, ParseContext)} so the configured policy applies. Unlike this
* method, it keeps a bare {@link TikaException} wrapper.
*/
@Deprecated
public static String getFilteredStackTrace(Throwable t) {
//legacy semantics: a bare TikaException wrapper is stripped, subclasses are not
Throwable unwrapped = t.getClass().equals(TikaException.class) && t.getCause() != null ?
t.getCause() : t;
return format(unwrapped, new ExceptionReporting());
}
/**
* @deprecated since 4.1, removal planned for 5.0; use
* {@link #format(Throwable, ParseContext)} so the configured policy applies
*/
@Deprecated
public static String getStackTrace(Throwable t) {
return format(t, new ExceptionReporting());
}
private static String full(Throwable t) {
StringWriter result = new StringWriter();
try (PrintWriter writer = new PrintWriter(result)) {
t.printStackTrace(writer);
}
return result.toString();
}
/**
* Truncates {@code s} to the policy's {@link ExceptionReporting#getMaxLength()} (no-op when
* unlimited), for channels that report exception text they did not format themselves.
*/
public static String truncate(String s, int maxLength) {
if (maxLength < 0 || s.length() <= maxLength) {
return s;
}
int cut = maxLength;
//don't split a surrogate pair
if (Character.isHighSurrogate(s.charAt(cut - 1))) {
cut--;
}
return s.substring(0, cut) + TRUNCATED;
}
/**
* Mirrors {@link Throwable#printStackTrace} (frame elision, suppressed, causes, cycle
* guard) but never calls {@code toString()}/{@code getMessage()}; with {@code frames}
* false only the class-name chain is emitted.
*/
private static String redacted(Throwable t, boolean frames) {
StringBuilder sb = new StringBuilder();
Set<Throwable> seen = Collections.newSetFromMap(new IdentityHashMap<>());
seen.add(t);
sb.append(t.getClass().getName()).append('\n');
StackTraceElement[] trace = t.getStackTrace();
if (frames) {
for (StackTraceElement e : trace) {
sb.append("\tat ").append(e).append('\n');
}
}
for (Throwable s : t.getSuppressed()) {
enclosed(s, trace, "Suppressed: ", "\t", seen, sb, frames, 1);
}
Throwable cause = t.getCause();
if (cause != null) {
enclosed(cause, trace, "Caused by: ", "", seen, sb, frames, 1);
}
return sb.toString();
}
private static void enclosed(Throwable t, StackTraceElement[] enclosingTrace, String caption,
String prefix, Set<Throwable> seen, StringBuilder sb,
boolean frames, int depth) {
if (seen.contains(t)) {
sb.append(prefix).append(caption).append("[CIRCULAR REFERENCE: ")
.append(t.getClass().getName()).append("]\n");
return;
}
if (depth > MAX_CAUSE_DEPTH) {
sb.append(prefix).append("... cause chain truncated\n");
return;
}
seen.add(t);
sb.append(prefix).append(caption).append(t.getClass().getName()).append('\n');
StackTraceElement[] trace = t.getStackTrace();
if (frames) {
int m = trace.length - 1;
int n = enclosingTrace.length - 1;
while (m >= 0 && n >= 0 && trace[m].equals(enclosingTrace[n])) {
m--;
n--;
}
int framesInCommon = trace.length - 1 - m;
for (int i = 0; i <= m; i++) {
sb.append(prefix).append("\tat ").append(trace[i]).append('\n');
}
if (framesInCommon != 0) {
sb.append(prefix).append("\t... ").append(framesInCommon).append(" more\n");
}
}
for (Throwable s : t.getSuppressed()) {
enclosed(s, trace, "Suppressed: ", prefix + "\t", seen, sb, frames, depth + 1);
}
Throwable cause = t.getCause();
if (cause != null) {
enclosed(cause, trace, "Caused by: ", prefix, seen, sb, frames, depth + 1);
}
}
}