// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.tools.bugreport;
import java.io.PrintWriter;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.function.Supplier;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.tools.StreamUtils;
/**
* This is a special exception that cannot be directly thrown.
* <p>
* It is used to capture more information about an exception that was already thrown.
*
* @author Michael Zangl
* @see BugReport
* @since 10285
*/
public class ReportedException extends RuntimeException {
/**
* How many entries of a collection to include in the bug report.
*/
private static final int MAX_COLLECTION_ENTRIES = 30;
private static final long serialVersionUID = 737333873766201033L;
/**
* We capture all stack traces on exception creation. This allows us to trace synchonization problems better. We cannot be really sure what
* happened but we at least see which threads
*/
private final transient Map<Thread, StackTraceElement[]> allStackTraces;
private final LinkedList<Section> sections = new LinkedList<>();
private final transient Thread caughtOnThread;
private String methodWarningFrom;
ReportedException(Throwable exception) {
this(exception, Thread.currentThread());
}
ReportedException(Throwable exception, Thread caughtOnThread) {
super(exception);
allStackTraces = Thread.getAllStackTraces();
this.caughtOnThread = caughtOnThread;
}
/**
* Displays a warning for this exception. The program can then continue normally. Does not block.
*/
public void warn() {
methodWarningFrom = BugReport.getCallingMethod(2);
try {
BugReportQueue.getInstance().submit(this);
} catch (RuntimeException e) { // NOPMD
e.printStackTrace();
}
}
/**
* Starts a new debug data section. This normally does not need to be called manually.
*
* @param sectionName
* The section name.
*/
public void startSection(String sectionName) {
sections.add(new Section(sectionName));
}
/**
* Prints the captured data of this report to a {@link PrintWriter}.
*
* @param out
* The writer to print to.
*/
public void printReportDataTo(PrintWriter out) {
out.println("=== REPORTED CRASH DATA ===");
for (Section s : sections) {
s.printSection(out);
out.println();
}
if (methodWarningFrom != null) {
out.println("Warning issued by: " + methodWarningFrom);
out.println();
}
}
/**
* Prints the stack trace of this report to a {@link PrintWriter}.
*
* @param out
* The writer to print to.
*/
public void printReportStackTo(PrintWriter out) {
out.println("=== STACK TRACE ===");
out.println(niceThreadName(caughtOnThread));
getCause().printStackTrace(out);
out.println();
}
/**
* Prints the stack traces for other threads of this report to a {@link PrintWriter}.
*
* @param out
* The writer to print to.
*/
public void printReportThreadsTo(PrintWriter out) {
out.println("=== RUNNING THREADS ===");
for (Entry<Thread, StackTraceElement[]> thread : allStackTraces.entrySet()) {
out.println(niceThreadName(thread.getKey()));
if (caughtOnThread.equals(thread.getKey())) {
out.println("Stacktrace see above.");
} else {
for (StackTraceElement e : thread.getValue()) {
out.println(e);
}
}
out.println();
}
}
private static String niceThreadName(Thread thread) {
StringBuilder name = new StringBuilder("Thread: ").append(thread.getName()).append(" (").append(thread.getId()).append(')');
ThreadGroup threadGroup = thread.getThreadGroup();
if (threadGroup != null) {
name.append(" of ").append(threadGroup.getName());
}
return name.toString();
}
/**
* Checks if this exception is considered the same as an other exception. This is the case if both have the same cause and message.
*
* @param e
* The exception to check against.
* @return <code>true</code> if they are considered the same.
*/
public boolean isSame(ReportedException e) {
if (!getMessage().equals(e.getMessage())) {
return false;
}
return hasSameStackTrace(new CauseTraceIterator(), e.getCause());
}
private static boolean hasSameStackTrace(CauseTraceIterator causeTraceIterator, Throwable e2) {
if (!causeTraceIterator.hasNext()) {
// all done.
return true;
}
Throwable e1 = causeTraceIterator.next();
StackTraceElement[] t1 = e1.getStackTrace();
StackTraceElement[] t2 = e2.getStackTrace();
if (!Arrays.equals(t1, t2)) {
return false;
}
Throwable c1 = e1.getCause();
Throwable c2 = e2.getCause();
if ((c1 == null) != (c2 == null)) {
return false;
} else if (c1 != null) {
return hasSameStackTrace(causeTraceIterator, c2);
} else {
return true;
}
}
/**
* Adds some debug values to this exception. The value is converted to a string. Errors during conversion are handled.
*
* @param key
* The key to add this for. Does not need to be unique but it would be nice.
* @param value
* The value.
* @return This exception for easy chaining.
*/
public ReportedException put(String key, Object value) {
return put(key, () -> value);
}
/**
* Adds some debug values to this exception. This method automatically catches errors that occur during the production of the value.
*
* @param key
* The key to add this for. Does not need to be unique but it would be nice.
* @param valueSupplier
* A supplier that is called once to get the value.
* @return This exception for easy chaining.
* @since 10586
*/
public ReportedException put(String key, Supplier<Object> valueSupplier) {
String string;
try {
Object value = valueSupplier.get();
if (value == null) {
string = "null";
} else if (value instanceof Collection) {
string = makeCollectionNice((Collection<?>) value);
} else if (value.getClass().isArray()) {
string = makeCollectionNice(Arrays.asList(value));
} else {
string = value.toString();
}
} catch (RuntimeException t) { // NOPMD
Main.warn(t);
string = "<Error calling toString()>";
}
sections.getLast().put(key, string);
return this;
}
private static String makeCollectionNice(Collection<?> value) {
int lines = 0;
StringBuilder str = new StringBuilder(32);
for (Object e : value) {
str.append("\n - ");
if (lines <= MAX_COLLECTION_ENTRIES) {
str.append(e);
} else {
str.append("\n ... (")
.append(value.size())
.append(" entries)");
break;
}
}
return str.toString();
}
@Override
public String toString() {
return "ReportedException [thread=" + caughtOnThread + ", exception=" + getCause()
+ ", methodWarningFrom=" + methodWarningFrom + ']';
}
/**
* Check if this exception may be caused by a threading issue.
* @return <code>true</code> if it is.
* @since 10585
*/
public boolean mayHaveConcurrentSource() {
return StreamUtils.toStream(CauseTraceIterator::new)
.anyMatch(t -> t instanceof ConcurrentModificationException || t instanceof InvocationTargetException);
}
/**
* Check if this is caused by an out of memory situaition
* @return <code>true</code> if it is.
* @since 10819
*/
public boolean isOutOfMemory() {
return StreamUtils.toStream(CauseTraceIterator::new).anyMatch(t -> t instanceof OutOfMemoryError);
}
/**
* Iterates over the causes for this exception. Ignores cycles and aborts iteration then.
* @author Michal Zangl
* @since 10585
*/
private final class CauseTraceIterator implements Iterator<Throwable> {
private Throwable current = getCause();
private final Set<Throwable> dejaVu = Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>());
@Override
public boolean hasNext() {
return current != null;
}
@Override
public Throwable next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
Throwable toReturn = current;
advance();
return toReturn;
}
private void advance() {
dejaVu.add(current);
current = current.getCause();
if (current != null && dejaVu.contains(current)) {
current = null;
}
}
}
private static class SectionEntry implements Serializable {
private static final long serialVersionUID = 1L;
private final String key;
private final String value;
SectionEntry(String key, String value) {
this.key = key;
this.value = value;
}
/**
* Prints this entry to the output stream in a line.
* @param out The stream to print to.
*/
public void print(PrintWriter out) {
out.print(" - ");
out.print(key);
out.print(": ");
out.println(value);
}
}
private static class Section implements Serializable {
private static final long serialVersionUID = 1L;
private final String sectionName;
private final ArrayList<SectionEntry> entries = new ArrayList<>();
Section(String sectionName) {
this.sectionName = sectionName;
}
/**
* Add a key/value entry to this section.
* @param key The key. Need not be unique.
* @param value The value.
*/
public void put(String key, String value) {
entries.add(new SectionEntry(key, value));
}
/**
* Prints this section to the output stream.
* @param out The stream to print to.
*/
public void printSection(PrintWriter out) {
out.println(sectionName + ':');
if (entries.isEmpty()) {
out.println("No data collected.");
} else {
for (SectionEntry e : entries) {
e.print(out);
}
}
}
}
}