/******************************************************************************* * Copyright (c) 2012 VMWare, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VMWare, Inc. - initial API and implementation *******************************************************************************/ package org.grails.ide.eclipse.ui.console; import java.util.StringTokenizer; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.debug.ui.console.FileLink; import org.eclipse.debug.ui.console.IConsole; import org.eclipse.debug.ui.console.IConsoleLineTracker; import org.eclipse.jdt.internal.debug.ui.console.JavaStackTraceHyperlink; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.ui.console.TextConsole; import org.grails.ide.eclipse.core.GrailsCoreActivator; /** * Startgin with Grails 2.0 the stack traces are formatted differently. We need to ensure that the hyperlinks are still available * to go to the source of the stack frame. * * Should look something like this: * <pre> Line | Method ->> 303 | innerRun in java.util.concurrent.FutureTask$Sync - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | 138 | run in java.util.concurrent.FutureTask | 886 | runTask in java.util.concurrent.ThreadPoolExecutor$Worker | 908 | run in '' ^ 680 | run . . in java.lang.Thread </pre> * * @author Andrew Eisenberg * @since 2.8.0.M2 */ public class StackTraceConsoleLineTracker implements IConsoleLineTracker { public final class StackFrameInfo { public final String lineText; public final int lineNum; public final String fixedClassName; public final String method; public final String lineNumStr; public final String origClassName; StackFrameInfo(String lineText, int lineNum, String fixedClassName, String method, String lineNumStr, String origClassName) { this.lineNum = lineNum; this.lineNumStr = lineNumStr; this.fixedClassName = fixedClassName; this.method = method; this.origClassName = origClassName; this.lineText = lineText; } @Override public String toString() { return "StackFrameInfo [lineText=" + lineText + ", lineNum=" + lineNum + ", fixedClassName=" + fixedClassName + ", method=" + method + ", lineNumStr=" + lineNumStr + ", origClassName=" + origClassName + "]"; } } private final class GrailsStackTraceHyperlink extends JavaStackTraceHyperlink { private final StackFrameInfo info; public GrailsStackTraceHyperlink(StackFrameInfo info, TextConsole console) { super(console); this.info = info; } @Override protected int getLineNumber(String linkText) throws CoreException { return info.lineNum; } @Override protected String getTypeName(String linkText) throws CoreException { return info.fixedClassName; } @Override protected String getLinkText() throws CoreException { return info.fixedClassName; } } private final static String[] START_SEQUENCES = new String[] { "|", "->>", "^" }; private TextConsole console; private IDocument document; public void init(IConsole console) { if (console instanceof TextConsole) { this.console = (TextConsole) console; this.document = console.getDocument(); } else { this.console = null; this.document = null; } } protected void initDocument(IDocument document) { this.document = document; } public void lineAppended(IRegion line) { if (console == null) { return; } try { StackFrameInfo info = extractStackFrame(line, 0); if (info != null) { createLinks(line, info); } } catch (BadLocationException e) { GrailsCoreActivator.log(e); } } private String getLineText(IRegion line) throws BadLocationException { return document.get(line.getOffset(), line.getLength()); } /** * create 3 hyperlinks. One for the line number, one for the method name and * one for the type nme * @param line * @param info * @param text * @throws BadLocationException */ private void createLinks(IRegion line, StackFrameInfo info) throws BadLocationException { int lineStart = info.lineText.indexOf(info.lineNumStr); int methodStart = info.lineText.indexOf(info.method, lineStart); int classNameStart = info.lineText.indexOf(info.origClassName); if (info.origClassName.endsWith(".gsp")) { // must handle gsp files differently IFile file = findFile(info); if (file != null) { console.addHyperlink(new FileLink(file, null, -1, -1, info.lineNum), line.getOffset() + lineStart, info.lineNumStr.length()); console.addHyperlink(new FileLink(file, null, -1, -1, info.lineNum), line.getOffset() + methodStart, info.method.length()); console.addHyperlink(new FileLink(file, null, -1, -1, info.lineNum), line.getOffset() + classNameStart, info.origClassName.length()); } } else { console.addHyperlink(new GrailsStackTraceHyperlink(info, console), line.getOffset() + lineStart, info.lineNumStr.length()); console.addHyperlink(new GrailsStackTraceHyperlink(info, console), line.getOffset() + methodStart, info.method.length()); console.addHyperlink(new GrailsStackTraceHyperlink(info, console), line.getOffset() + classNameStart, info.origClassName.length()); } } private IFile findFile(StackFrameInfo info) { // this is the name of the file without the project name // need to gues the project name. // assume that this file is inside of the project (and not coming from an in place plugin) String name = info.origClassName; String consoleName = console.getName(); IPath path = new Path(consoleName); if (path.segmentCount() > 1) { // STS-2506 project name has a slash or some other unreadable character. return null; } int nameEnd = consoleName.indexOf(" ("); IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(consoleName.substring(0, nameEnd)); if (! project.exists()) { return null; } IFile file = project.getFile(name); return file.exists() ? file : null; } /** * @param region the region of the current line * @param depth not my favorite way of doing things, but use this param to ensure that recursion doesn't go too deep * @return enough information about the stack frame to create a link, or null if there is none. */ protected StackFrameInfo extractStackFrame(IRegion region, int depth) { if (depth >= 4) { // avoid extensive recursion return null; } String text = null; try { text = getLineText(region); } catch (BadLocationException e) { GrailsCoreActivator.log(e); return null; } if (!isValidStart(text)) { return null; } StringTokenizer tokenizer = new StringTokenizer(text); // ignore the first element tokenizer.nextToken(); if (!tokenizer.hasMoreElements()) { return null; } int lineNum = -1; String origClassName = null; String methodName = null; String next = tokenizer.nextToken(); String lineNumStr; String fixedClassName; // should be a number try { lineNumStr = next; lineNum = Integer.parseInt(next); } catch (NumberFormatException nfe) { return null; } if (!tokenizer.hasMoreElements()) { return null; } // column separator if (!tokenizer.nextToken().equals("|")) { return null; } if (!tokenizer.hasMoreElements()) { return null; } // method name methodName = tokenizer.nextToken(); if (!tokenizer.hasMoreElements()) { return null; } next = tokenizer.nextToken(); // there can be some dots here. just consume them while (next.equals(".") && tokenizer.hasMoreTokens()) { next = tokenizer.nextToken(); } // the word 'in' if (!next.equals("in")) { return null; } if (!tokenizer.hasMoreElements()) { return null; } // class name or '' origClassName = tokenizer.nextToken(); if (tokenizer.hasMoreElements()) { return null; } if (!origClassName.equals("''")) { fixedClassName = fixTypeName(origClassName); } else { // need to recursively check the previous line for the real type name fixedClassName = null; IRegion previous = getPrevious(region, depth); if (previous != null) { StackFrameInfo info = extractStackFrame(previous, depth+1); if (info != null) { fixedClassName = info.fixedClassName; } } } if (fixedClassName== null || fixedClassName.equals("''")) { // recursion has bottomed out return null; } return new StackFrameInfo(text, lineNum, fixedClassName, methodName, lineNumStr, origClassName); } private IRegion getPrevious(IRegion region, int depth) { if (depth > 4) { // prevent excessive recursion return null; } if (region.getOffset() <= 0) { return null; } try { int lineOfOffset = document.getLineOfOffset(region.getOffset()); if (lineOfOffset <= 0) { return null; } IRegion candidate = document.getLineInformation(lineOfOffset-1); if (document.get(candidate.getOffset(), candidate.getLength()).startsWith("- - -")) { return getPrevious(candidate, depth+1); } return candidate; } catch (BadLocationException e) { GrailsCoreActivator.log(e); return null; } } private String fixTypeName(String className) { if (className.contains("__closure")) { // assume synthetic closure call int index = className.indexOf('$'); if (index > 0) { className = className.substring(0, index); } } return className.replace('$', '.'); } public void dispose() { console = null; document = null; } private boolean isValidStart(String text) { for (String prefix : START_SEQUENCES) { if (text.startsWith(prefix)) { return true; } } return false; } }