Bug 442781 - Support hyperlinks for stack entries with method signature

JVMs like SAP JVM may add method signatures in stack traces.  Correctly
parse these stack entries in JDT's stack trace console.

Also add lighweight tests for hyperlink detection for both classic and
extended stack entries.

Change-Id: I25c3039deda3d6b77b4d00e41e692988cb5b0aa2
Signed-off-by: Christian Georgi <christian.georgi@sap.com>
diff --git a/org.eclipse.jdt.debug.tests/console tests/org/eclipse/jdt/debug/tests/console/JavaStackTraceConsoleTest.java b/org.eclipse.jdt.debug.tests/console tests/org/eclipse/jdt/debug/tests/console/JavaStackTraceConsoleTest.java
new file mode 100644
index 0000000..a9bfee7
--- /dev/null
+++ b/org.eclipse.jdt.debug.tests/console tests/org/eclipse/jdt/debug/tests/console/JavaStackTraceConsoleTest.java
@@ -0,0 +1,142 @@
+/*******************************************************************************
+ * Copyright (c) 2014 SAP SE and others.
+ * 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:
+ *     SAP SE - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.jdt.debug.tests.console;
+
+import static org.junit.Assert.assertArrayEquals;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jdt.debug.tests.AbstractDebugTest;
+import org.eclipse.jdt.internal.debug.ui.console.JavaStackTraceConsole;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.BadPositionCategoryException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.Position;
+import org.eclipse.ui.console.ConsolePlugin;
+import org.eclipse.ui.console.IConsole;
+import org.eclipse.ui.console.IConsoleManager;
+import org.eclipse.ui.internal.console.ConsoleHyperlinkPosition;
+
+/**
+ * Tests {@link JavaStackTraceConsole}
+ */
+public class JavaStackTraceConsoleTest extends AbstractDebugTest {
+
+	private JavaStackTraceConsole fConsole;
+
+	public JavaStackTraceConsoleTest(String name) {
+		super(name);
+	}
+
+	@Override
+	public void setUp() throws Exception {
+		super.setUp();
+
+		IConsoleManager consoleManager = ConsolePlugin.getDefault().getConsoleManager();
+		fConsole = new JavaStackTraceConsole();
+		consoleManager.addConsoles(new IConsole[] { fConsole });
+	}
+
+	@Override
+	protected void tearDown() throws Exception {
+		IConsoleManager consoleManager = ConsolePlugin.getDefault().getConsoleManager();
+		consoleManager.removeConsoles(new IConsole[] { fConsole });
+
+		super.tearDown();
+	}
+
+	public void testHyperlinkMatchSignatureSimple() throws Exception {
+		consoleDocumentWithText("at foo.bar.Type.method1(Type.java:1)");
+
+		String[] matchTexts = linkTextsAtPositions(24);
+		assertArrayEquals(allLinks(), new String[] { "Type.java:1" }, matchTexts);
+	}
+
+	public void testHyperlinkMatchSignatureExtended() throws Exception {
+		consoleDocumentWithText("at foo.bar.Type.method1(IILjava/lang/String;)V(Type.java:1)");
+
+		String[] matchTexts = linkTextsAtPositions(47);
+		assertArrayEquals(allLinks(), new String[] { "Type.java:1" }, matchTexts);
+	}
+
+	public void testHyperlinkMatchMultiple() throws Exception {
+		consoleDocumentWithText("at foo.bar.Type.method2(Type.java:2)\n" //
+				+ "at foo.bar.Type.method1(Type.java:1)");
+
+		String[] matchTexts = linkTextsAtPositions(24, 61);
+		assertArrayEquals(allLinks(), new String[] { "Type.java:2", "Type.java:1" }, matchTexts);
+	}
+
+	public void testHyperlinkMatchInvalidLine() throws Exception {
+		consoleDocumentWithText("at foo.bar.Type.method1(Type.java:fff)");
+
+		String[] matchTexts = linkTextsAtPositions(24);
+		assertArrayEquals(allLinks(), new String[] { "Type.java:fff" }, matchTexts);
+	}
+
+	public void testHyperlinkNoMatch() throws Exception {
+		consoleDocumentWithText("at foo.bar.Type.method1(foo.bar.Type.java:42)");
+
+		Position[] positions = allLinkPositions();
+		assertArrayEquals("Expected no hyperlinks for invalid type name", new Position[0], positions);
+	}
+
+	private IDocument consoleDocumentWithText(String text) throws InterruptedException {
+		IDocument document = fConsole.getDocument();
+		document.set(text);
+		// wait for document being parsed and hyperlinks created
+		Job.getJobManager().join(fConsole, null);
+		return document;
+	}
+
+	private String[] linkTextsAtPositions(int... offsets) throws BadLocationException {
+		IDocument document = fConsole.getDocument();
+
+		List<String> texts = new ArrayList<String>(offsets.length);
+		List<Position> positions = linkPositions(offsets);
+		for (Position pos : positions) {
+			String matchText = document.get(pos.getOffset(), pos.getLength());
+			texts.add(matchText);
+		}
+		return texts.toArray(new String[texts.size()]);
+	}
+
+	private List<Position> linkPositions(int... offsets) {
+		List<Position> filteredPositions = new ArrayList<Position>(offsets.length);
+		for (Position position : allLinkPositions()) {
+			for (int offset : offsets) {
+				if (offset >= position.getOffset() && offset <= (position.getOffset() + position.getLength())) {
+					filteredPositions.add(position);
+					break;
+				}
+			}
+		}
+		return filteredPositions;
+	}
+
+	private Position[] allLinkPositions() {
+		try {
+			return fConsole.getDocument().getPositions(ConsoleHyperlinkPosition.HYPER_LINK_CATEGORY);
+		}
+		catch (BadPositionCategoryException ex) {
+			// no hyperlinks
+		}
+		return new Position[0];
+	}
+
+	private String allLinks() {
+		return Arrays.toString(allLinkPositions());
+	}
+
+}
diff --git a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AutomatedSuite.java b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AutomatedSuite.java
index 5eddeb0..4cb0bc0 100644
--- a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AutomatedSuite.java
+++ b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AutomatedSuite.java
@@ -7,6 +7,7 @@
  *
  * Contributors:
  *     IBM Corporation - initial API and implementation
+ *     SAP SE - Support hyperlinks for stack entries with method signature
  *******************************************************************************/
 package org.eclipse.jdt.debug.tests;
 
@@ -44,6 +45,7 @@
 import org.eclipse.jdt.debug.tests.breakpoints.WatchpointTests;
 import org.eclipse.jdt.debug.tests.console.ConsoleTerminateAllActionTests;
 import org.eclipse.jdt.debug.tests.console.IOConsoleTests;
+import org.eclipse.jdt.debug.tests.console.JavaStackTraceConsoleTest;
 import org.eclipse.jdt.debug.tests.core.ArgumentTests;
 import org.eclipse.jdt.debug.tests.core.ArrayTests;
 import org.eclipse.jdt.debug.tests.core.BootpathTests;
@@ -248,14 +250,15 @@
 		addTest(new TestSuite(EnvironmentTests.class));
 		addTest(new TestSuite(ExecutionEnvironmentTests.class));
 		addTest(new TestSuite(ArgumentTests.class));
-		
+
 	//Console tests
 		addTest(new TestSuite(ConsoleTests.class));
 		addTest(new TestSuite(ConsoleInputTests.class));
 		addTest(new TestSuite(LineTrackerTests.class));
+		addTest(new TestSuite(JavaStackTraceConsoleTest.class));
 		addTest(new TestSuite(IOConsoleTests.class));
 		addTest(new TestSuite(ConsoleTerminateAllActionTests.class));
-		
+
 	//Core tests
 		addTest(new TestSuite(DebugEventTests.class));
 		addTest(new TestSuite(EventSetTests.class));
diff --git a/org.eclipse.jdt.debug.ui/plugin.xml b/org.eclipse.jdt.debug.ui/plugin.xml
index 20ba1d3..b6c176e 100644
--- a/org.eclipse.jdt.debug.ui/plugin.xml
+++ b/org.eclipse.jdt.debug.ui/plugin.xml
@@ -6,14 +6,15 @@
      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:
          IBM Corporation - initial API and implementation
+         SAP SE - Support hyperlinks for stack entries with method signature
  -->
 
 <plugin>
 
-    
+
 <!-- Extensions Points -->
    <extension-point id="vmInstallTypePage" name="%vmInstallTypePage" schema="schema/vmInstallTypePage.exsd"/>
    <extension-point id="vmInstallPages" name="%vmInstallPage" schema="schema/vmInstallPages.exsd"/>
@@ -3340,14 +3341,14 @@
             class="org.eclipse.jdt.internal.debug.ui.console.JavaStackTraceConsoleFactory">
       </consoleFactory>
    </extension>
-   
-   
+
+
    <!--Java Stack Trace Pattern Matchers-->
    <extension
          point="org.eclipse.ui.console.consolePatternMatchListeners">
       <consolePatternMatchListener
             class="org.eclipse.jdt.internal.debug.ui.console.JavaConsoleTracker"
-            regex="\(\S*${java_extensions_regex}\S*\)"
+            regex="\(\w*${java_extensions_regex}\S*\)"
             qualifier="${java_extensions_regex}"
             id="org.eclipse.jdt.debug.ui.JavaConsoleTracker">
          <enablement>
diff --git a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/console/JavaStackTraceHyperlink.java b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/console/JavaStackTraceHyperlink.java
index b669ddb..83293c9 100644
--- a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/console/JavaStackTraceHyperlink.java
+++ b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/console/JavaStackTraceHyperlink.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2012 IBM Corporation and others.
+ * Copyright (c) 2000, 2014 IBM Corporation and others.
  * 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
@@ -7,6 +7,7 @@
  *
  * Contributors:
  *     IBM Corporation - initial API and implementation
+ *     SAP SE - Support hyperlinks for stack entries with method signature
  *******************************************************************************/
 package org.eclipse.jdt.internal.debug.ui.console;
 
@@ -201,20 +202,22 @@
 
 	/**
 	 * Returns the fully qualified name of the type to open
-	 * 
-	 * @param linkText the complete text of the link to be parsed
+	 *
+	 * @param linkText
+	 *            the complete text of the link to be parsed
 	 * @return fully qualified type name
-	 * @exception CoreException if unable to parse the type name
+	 * @exception CoreException
+	 *                if unable to parse the type name
 	 */
 	protected String getTypeName(String linkText) throws CoreException {
-        int start = linkText.indexOf('(');
+		int start = linkText.lastIndexOf('(');
         int end = linkText.indexOf(':');
         if (start >= 0 && end > start) {
             //linkText could be something like packageA.TypeB(TypeA.java:45)
             //need to look in packageA.TypeA for line 45 since TypeB is defined
-            //in TypeA.java 
+            //in TypeA.java
             //Inner classes can be ignored because we're using file and line number
-            
+
             // get File name (w/o .java)
             String typeName = linkText.substring(start + 1, end);
             typeName = JavaCore.removeJavaLikeExtension(typeName);