Skip to content

Commit 991b895

Browse files
vrubezhnybenoitf
authored andcommitted
CHE-15 - Java stacktrace support (From Platform to Che Workspace) (eclipse-che#5396)
* CHE-15 - Java stacktrace support (From Platform to Che Workspace) Adds the possibility to click on a stacktrace line in order to open a specified class in editor. Also fixes CHE-293 (eclipse-che#5001) - String Written to Dev-Machine stdout not Escaped Properly Issue: https://issues.jboss.org/browse/CHE-15 Issue: https://issues.jboss.org/browse/CHE-293 see: eclipse-che#5001 Signed-off-by: Victor Rubezhny <vrubezhny@redhat.com>
1 parent c7c1ee8 commit 991b895

8 files changed

Lines changed: 402 additions & 4 deletions

File tree

ide/che-core-ide-app/pom.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,9 @@
336336
<exclude>**/*.png</exclude>
337337
<exclude>**/*.gif</exclude>
338338
<exclude>**/*.jpg</exclude>
339+
<exclude>**/OutputCustomizer.java</exclude>
340+
<exclude>**/DefaultOutputCustomizer.java</exclude>
341+
<exclude>**/DefaultOutputCustomizerTest.java</exclude>
339342
</excludes>
340343
</configuration>
341344
</plugin>

ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/console/CommandOutputConsolePresenter.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@
2525
import org.eclipse.che.api.promises.client.Operation;
2626
import org.eclipse.che.api.promises.client.OperationException;
2727
import org.eclipse.che.commons.annotation.Nullable;
28+
import org.eclipse.che.ide.api.app.AppContext;
2829
import org.eclipse.che.ide.api.command.CommandExecutor;
2930
import org.eclipse.che.ide.api.command.CommandImpl;
31+
import org.eclipse.che.ide.api.editor.EditorAgent;
3032
import org.eclipse.che.ide.api.machine.ExecAgentCommandManager;
3133
import org.eclipse.che.ide.api.machine.events.ProcessFinishedEvent;
3234
import org.eclipse.che.ide.api.machine.events.ProcessStartedEvent;
@@ -66,6 +68,8 @@ public class CommandOutputConsolePresenter implements CommandOutputConsole, Outp
6668
private boolean followOutput = true;
6769

6870
private final List<ActionDelegate> actionDelegates = new ArrayList<>();
71+
72+
private OutputCustomizer outputCustomizer = null;
6973

7074
@Inject
7175
public CommandOutputConsolePresenter(final OutputConsoleView view,
@@ -75,7 +79,9 @@ public CommandOutputConsolePresenter(final OutputConsoleView view,
7579
EventBus eventBus,
7680
ExecAgentCommandManager execAgentCommandManager,
7781
@Assisted CommandImpl command,
78-
@Assisted Machine machine) {
82+
@Assisted Machine machine,
83+
AppContext appContext,
84+
EditorAgent editorAgent) {
7985
this.view = view;
8086
this.resources = resources;
8187
this.execAgentCommandManager = execAgentCommandManager;
@@ -84,6 +90,7 @@ public CommandOutputConsolePresenter(final OutputConsoleView view,
8490
this.eventBus = eventBus;
8591
this.commandExecutor = commandExecutor;
8692

93+
setCustomizer(new DefaultOutputCustomizer(appContext, editorAgent));
8794
view.setDelegate(this);
8895

8996
final String previewUrl = command.getAttributes().get(COMMAND_PREVIEW_URL_ATTRIBUTE_NAME);
@@ -275,4 +282,13 @@ public String getText() {
275282
return view.getText();
276283
}
277284

285+
@Override
286+
public OutputCustomizer getCustomizer() {
287+
return outputCustomizer;
288+
}
289+
290+
/** Sets up the text output customizer */
291+
public void setCustomizer(OutputCustomizer customizer) {
292+
this.outputCustomizer = customizer;
293+
}
278294
}

ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/console/DefaultOutputConsole.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import com.google.inject.Inject;
1515
import com.google.inject.assistedinject.Assisted;
1616

17+
import org.eclipse.che.ide.api.app.AppContext;
18+
import org.eclipse.che.ide.api.editor.EditorAgent;
1719
import org.eclipse.che.ide.api.outputconsole.OutputConsole;
1820
import org.eclipse.che.ide.machine.MachineResources;
1921
import org.vectomatic.dom.svg.ui.SVGResource;
@@ -39,15 +41,21 @@ public class DefaultOutputConsole implements OutputConsole, OutputConsoleView.Ac
3941
/** Follow output when printing text */
4042
private boolean followOutput = true;
4143

44+
private OutputCustomizer customizer = null;
45+
4246
@Inject
4347
public DefaultOutputConsole(OutputConsoleView view,
4448
MachineResources resources,
49+
AppContext appContext,
50+
EditorAgent editorAgent,
4551
@Assisted String title) {
4652
this.view = view;
4753
this.title = title;
4854
this.resources = resources;
4955
this.view.enableAutoScroll(true);
5056

57+
setCustomizer(new DefaultOutputCustomizer(appContext, editorAgent));
58+
5159
view.setDelegate(this);
5260

5361
view.hideCommand();
@@ -182,4 +190,14 @@ public void onOutputScrolled(boolean bottomReached) {
182190
view.toggleScrollToEndButton(bottomReached);
183191
}
184192

193+
@Override
194+
public OutputCustomizer getCustomizer() {
195+
return customizer;
196+
}
197+
198+
/** Sets up the text output customizer */
199+
public void setCustomizer(OutputCustomizer customizer) {
200+
this.customizer = customizer;
201+
}
202+
185203
}
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2017 RedHat, Inc.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* http://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* RedHat, Inc. - initial API and implementation
10+
*******************************************************************************/
11+
package org.eclipse.che.ide.console;
12+
13+
import static com.google.common.base.Preconditions.checkNotNull;
14+
import static com.google.common.base.Strings.nullToEmpty;
15+
import static com.google.gwt.regexp.shared.RegExp.compile;
16+
17+
import java.util.List;
18+
import java.util.stream.Collectors;
19+
import java.util.stream.Stream;
20+
21+
import org.eclipse.che.api.promises.client.Function;
22+
import org.eclipse.che.api.promises.client.FunctionException;
23+
import org.eclipse.che.api.promises.client.Promise;
24+
import org.eclipse.che.ide.api.app.AppContext;
25+
import org.eclipse.che.ide.api.editor.EditorAgent;
26+
import org.eclipse.che.ide.api.editor.EditorPartPresenter;
27+
import org.eclipse.che.ide.api.editor.OpenEditorCallbackImpl;
28+
import org.eclipse.che.ide.api.editor.text.TextPosition;
29+
import org.eclipse.che.ide.api.editor.text.TextRange;
30+
import org.eclipse.che.ide.api.editor.texteditor.TextEditor;
31+
import org.eclipse.che.ide.api.resources.Container;
32+
import org.eclipse.che.ide.api.resources.File;
33+
import org.eclipse.che.ide.api.resources.Resource;
34+
import org.eclipse.che.ide.resource.Path;
35+
36+
import com.google.gwt.regexp.shared.MatchResult;
37+
import com.google.gwt.regexp.shared.RegExp;
38+
import com.google.gwt.user.client.Timer;
39+
import com.google.inject.Inject;
40+
41+
/**
42+
* Default customizer adds an anchor link to the lines that match a stack trace
43+
* line pattern and installs a handler function for the link. The handler parses
44+
* the stack trace line, searches for the candidate Java files to navigate to,
45+
* opens the first file (of the found candidates) in editor and reveals it to
46+
* the required line according to the stack trace line information
47+
*/
48+
public class DefaultOutputCustomizer implements OutputCustomizer {
49+
50+
private static final RegExp LINE_AT = compile("(\\s+at .+)");
51+
private static final RegExp LINE_AT_EXCEPTION = compile("(\\s+at address:.+)");
52+
53+
private AppContext appContext;
54+
private EditorAgent editorAgent;
55+
56+
@Inject
57+
public DefaultOutputCustomizer(AppContext appContext, EditorAgent editorAgent) {
58+
this.appContext = appContext;
59+
this.editorAgent = editorAgent;
60+
61+
exportAnchorClickHandlerFunction();
62+
}
63+
64+
/*
65+
* (non-Javadoc)
66+
*
67+
* @see org.eclipse.che.ide.extension.machine.client.outputspanel.console.
68+
* OutputCustomizer#canCustomize(java.lang.String)
69+
*/
70+
@Override
71+
public boolean canCustomize(String text) {
72+
return (LINE_AT.exec(text) != null && LINE_AT_EXCEPTION.exec(text) == null);
73+
}
74+
75+
/*
76+
* (non-Javadoc)
77+
*
78+
* @see org.eclipse.che.ide.extension.machine.client.outputspanel.console.
79+
* OutputCustomizer#customize(java.lang.String)
80+
*/
81+
@Override
82+
public String customize(String text) {
83+
String customText = text;
84+
85+
MatchResult matcher = LINE_AT.exec(text);
86+
if (matcher != null) {
87+
try {
88+
int start = text.indexOf("at", 0) + "at".length(), openBracket = text.indexOf("(", start),
89+
column = text.indexOf(":", openBracket), closingBracket = text.indexOf(")", column);
90+
String qualifiedName = text.substring(start, openBracket).trim();
91+
String fileName = text.substring(openBracket + "(".length(), column).trim();
92+
int lineNumber = Integer.valueOf(text.substring(column + ":".length(), closingBracket).trim());
93+
customText = text.substring(0, openBracket + "(".length());
94+
customText += "<a href='javascript:open(\"" + qualifiedName + "\", \"" + fileName + "\", " + lineNumber
95+
+ ");'>";
96+
customText += text.substring(openBracket + "(".length(), closingBracket);
97+
customText += "</a>";
98+
customText += text.substring(closingBracket);
99+
text = customText;
100+
} catch (IndexOutOfBoundsException ex) {
101+
// ignore
102+
}
103+
}
104+
105+
return text;
106+
}
107+
108+
/**
109+
* A callback that is to be called for an anchor
110+
*
111+
* @param qualifiedName
112+
* @param fileName
113+
* @param lineNumber
114+
*/
115+
public void handleAnchorClick(String qualifiedName, String fileName, final int lineNumber) {
116+
if (qualifiedName == null || fileName == null) {
117+
return;
118+
}
119+
120+
String qualifiedClassName = qualifiedName.lastIndexOf('.') != -1
121+
? qualifiedName.substring(0, qualifiedName.lastIndexOf('.'))
122+
: qualifiedName;
123+
final String packageName = qualifiedClassName.lastIndexOf('.') != -1
124+
? qualifiedClassName.substring(0, qualifiedClassName.lastIndexOf('.'))
125+
: "";
126+
127+
String relativeFilePath = (packageName.isEmpty() ? "" :
128+
(packageName.replace(".", "/") + "/")) + fileName;
129+
130+
collectChildren(appContext.getWorkspaceRoot(), Path.valueOf(relativeFilePath)).then(files -> {
131+
if (!files.isEmpty()) {
132+
editorAgent.openEditor(files.get(0), new OpenEditorCallbackImpl() {
133+
@Override
134+
public void onEditorOpened(EditorPartPresenter editor) {
135+
Timer t = new Timer() {
136+
@Override
137+
public void run() {
138+
EditorPartPresenter editorPart = editorAgent.getActiveEditor();
139+
selectRange(editorPart, lineNumber);
140+
}
141+
};
142+
t.schedule(500);
143+
}
144+
145+
@Override
146+
public void onEditorActivated(EditorPartPresenter editor) {
147+
selectRange(editor, lineNumber);
148+
}
149+
});
150+
151+
}
152+
});
153+
}
154+
155+
/*
156+
* Returns the list of workspace files filtered by a relative path
157+
*/
158+
private Promise<List<File>> collectChildren(Container root, Path relativeFilePath) {
159+
return root.getTree(-1).then(new Function<Resource[], List<File>>() {
160+
@Override
161+
public List<File> apply(Resource[] children) throws FunctionException {
162+
return Stream.of(children).filter(
163+
child -> child.isFile() && endsWith(child.asFile().getLocation(), relativeFilePath))
164+
.map(Resource::asFile).collect(Collectors.toList());
165+
}
166+
});
167+
}
168+
169+
/*
170+
* Checks if a path's last segments are equal to the provided relative path
171+
*/
172+
private boolean endsWith(Path path, Path relativePath) {
173+
checkNotNull(path);
174+
checkNotNull(relativePath);
175+
176+
if (path.segmentCount() < relativePath.segmentCount())
177+
return false;
178+
179+
for (int i = relativePath.segmentCount() - 1, j = path.segmentCount() - 1; i >= 0; i--, j--) {
180+
if (!nullToEmpty(relativePath.segment(i)).equals(path.segment(j))) {
181+
return false;
182+
}
183+
}
184+
185+
return true;
186+
}
187+
188+
/*
189+
* Selects and shows the specified line of text in editor
190+
*/
191+
private void selectRange(EditorPartPresenter editor, int line) {
192+
if (editor instanceof TextEditor) {
193+
TextPosition startPosition = new TextPosition(line - 1, 0);
194+
int lineOffsetStart = ((TextEditor) editor).getDocument().getLineStart(line - 1);
195+
if (lineOffsetStart == -1) {
196+
lineOffsetStart = 0;
197+
}
198+
199+
int lineOffsetEnd = ((TextEditor) editor).getDocument().getLineStart(line);
200+
if (lineOffsetEnd == -1) {
201+
lineOffsetEnd = 0;
202+
}
203+
while (((TextEditor) editor).getDocument().getLineAtOffset(lineOffsetEnd) > line - 1) {
204+
lineOffsetEnd--;
205+
}
206+
if (lineOffsetStart > lineOffsetEnd) {
207+
lineOffsetEnd = lineOffsetStart;
208+
}
209+
210+
TextPosition endPosition = new TextPosition(line - 1, lineOffsetEnd - lineOffsetStart);
211+
212+
((TextEditor) editor).getDocument().setSelectedRange(new TextRange(startPosition, endPosition), true);
213+
((TextEditor) editor).getDocument().setCursorPosition(startPosition);
214+
}
215+
}
216+
217+
/**
218+
* Sets up a java callback to be called for an anchor
219+
*/
220+
public native void exportAnchorClickHandlerFunction() /*-{
221+
var that = this;
222+
$wnd.open = $entry(function(qualifiedName,fileName,lineNumber) {
223+
that.@org.eclipse.che.ide.console.DefaultOutputCustomizer::handleAnchorClick(*)(qualifiedName,fileName,lineNumber);
224+
});
225+
}-*/;
226+
}

ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/console/OutputConsoleView.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ interface ActionDelegate {
159159
/** Handle scrolling the output. */
160160
void onOutputScrolled(boolean bottomReached);
161161

162+
/** Returns the customizer for the console output */
163+
OutputCustomizer getCustomizer();
164+
162165
}
163166

164167
}

ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/console/OutputConsoleViewImpl.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.google.gwt.regexp.shared.MatchResult;
2525
import com.google.gwt.regexp.shared.RegExp;
2626
import com.google.gwt.safehtml.shared.SafeHtml;
27+
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
2728
import com.google.gwt.uibinder.client.UiBinder;
2829
import com.google.gwt.uibinder.client.UiField;
2930
import com.google.gwt.user.client.DOM;
@@ -336,16 +337,25 @@ public String asString() {
336337
return " ";
337338
}
338339

340+
String encoded = SafeHtmlUtils.htmlEscape(text);
341+
if (delegate != null) {
342+
if (delegate.getCustomizer() != null) {
343+
if (delegate.getCustomizer().canCustomize(encoded)) {
344+
encoded = delegate.getCustomizer().customize(encoded);
345+
}
346+
}
347+
}
348+
339349
for (final Pair<RegExp, String> pair : output2Color) {
340-
final MatchResult matcher = pair.first.exec(text);
350+
final MatchResult matcher = pair.first.exec(encoded);
341351

342352
if (matcher != null) {
343-
return text.replaceAll(matcher.getGroup(1),
353+
return encoded.replaceAll(matcher.getGroup(1),
344354
"<span style=\"color: " + pair.second + "\">" + matcher.getGroup(1) + "</span>");
345355
}
346356
}
347357

348-
return text;
358+
return encoded;
349359
}
350360
};
351361

0 commit comments

Comments
 (0)