io.jooby.internal.whoops.Frame Maven / Gradle / Ivy
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby.internal.whoops;
import static java.util.Optional.ofNullable;
import java.io.File;
import java.nio.file.Files;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Frame {
private static final int SAMPLE_SIZE = 10;
private String fileName;
private String methodName;
private int lineStart;
private int line;
private String location;
private String source;
private boolean open;
private String className;
private List comments;
private Frame() {}
public String getFileName() {
return fileName;
}
public String getMethodName() {
return methodName;
}
public int getLineStart() {
return lineStart;
}
public int getLine() {
return line;
}
public String getLocation() {
return location;
}
public String getSource() {
return source;
}
public boolean isOpen() {
return open;
}
public String getClassName() {
return className;
}
public List getComments() {
return comments;
}
public boolean hasSource() {
return source != null && source.length() > 0;
}
public static List toFrames(SourceLocator locator, Throwable cause) {
LinkedList causalChain = getCausalChain(cause);
Throwable head = causalChain.getLast();
List frames =
causalChain.stream()
.filter(it -> it != head)
.map(it -> toFrame(locator, it, it.getStackTrace()[0]))
.collect(Collectors.toList());
Stream.of(head.getStackTrace()).map(e -> toFrame(locator, head, e)).forEach(frames::add);
// Keep application frames (ignore all others)
return frames.stream().filter(Frame::hasSource).collect(Collectors.toList());
}
static Frame toFrame(
final SourceLocator locator, final Throwable cause, final StackTraceElement e) {
int line = Math.max(e.getLineNumber(), 1);
String className = ofNullable(e.getClassName()).orElse("~unknown");
String[] names = className.split("\\.");
String filename = ofNullable(e.getFileName()).orElse(names[names.length - 1]);
StringBuilder path = new StringBuilder();
Stream.of(names).limit(names.length - 1).forEach(it -> path.append(it).append(File.separator));
path.append(names[names.length - 1]);
SourceLocator.Source source = locator.source(path.toString());
SourceLocator.Preview preview = source.preview(line, SAMPLE_SIZE);
Frame frame = new Frame();
frame.fileName = filename;
frame.methodName = ofNullable(e.getMethodName()).orElse("~unknown");
frame.lineStart = preview.getLineStart();
frame.line = line;
frame.location =
Files.exists(source.getPath())
? locator.getBasedir().relativize(source.getPath()).toString()
: filename;
frame.source = preview.getCode();
frame.open = false;
frame.className =
className
// clean up kotlin generated class name: App$1$1 => App
.replaceAll("\\$\\d+", "");
frame.comments = Collections.singletonList(cause);
return frame;
}
private static LinkedList getCausalChain(Throwable throwable) {
LinkedList causes = new LinkedList<>();
causes.add(throwable);
// Keep a second pointer that slowly walks the causal chain. If the fast pointer ever catches
// the slower pointer, then there's a loop.
Throwable slowPointer = throwable;
boolean advanceSlowPointer = false;
Throwable cause;
while ((cause = throwable.getCause()) != null) {
throwable = cause;
causes.add(throwable);
if (throwable == slowPointer) {
throw new IllegalArgumentException("Loop in causal chain detected.", throwable);
}
if (advanceSlowPointer) {
slowPointer = slowPointer.getCause();
}
advanceSlowPointer = !advanceSlowPointer; // only advance every other iteration
}
return causes;
}
}