/******************************************************************************* * Copyright 2011 Google Inc. All Rights Reserved. * * 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 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *******************************************************************************/ package com.google.gwt.eclipse.oophm.views.hierarchical; import com.google.gdt.eclipse.core.browser.BrowserUtilities; import com.google.gwt.eclipse.oophm.model.BrowserTab; import com.google.gwt.eclipse.oophm.model.IModelNode; import com.google.gwt.eclipse.oophm.model.LaunchConfiguration; import com.google.gwt.eclipse.oophm.model.Log; import com.google.gwt.eclipse.oophm.model.LogContentProvider; import com.google.gwt.eclipse.oophm.model.LogEntry; import com.google.gwt.eclipse.oophm.model.LogEntry.Data; import com.google.gwt.eclipse.oophm.model.LogLabelProvider; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.debug.core.model.IProcess; import org.eclipse.debug.ui.DebugUITools; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IPackageFragment; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.internal.debug.ui.console.JavaStackTraceHyperlink; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.ITreeSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TreePath; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerComparator; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.SashForm; import org.eclipse.swt.dnd.Clipboard; import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.events.KeyAdapter; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.MenuItem; import org.eclipse.ui.console.IConsole; import org.eclipse.ui.console.TextConsole; import org.eclipse.ui.dialogs.FilteredTree; import org.eclipse.ui.dialogs.PatternFilter; import org.eclipse.ui.forms.events.HyperlinkEvent; import org.eclipse.ui.forms.events.IHyperlinkListener; import org.eclipse.ui.forms.widgets.FormText; import org.eclipse.ui.forms.widgets.ScrolledFormText; import org.eclipse.wst.jsdt.internal.formatter.comment.Java2HTMLEntityReader; import java.io.IOException; import java.io.StringReader; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * The content pane for the log entries. * * @param <T> The entity (launch configuration, browser, or server) associated * with the log */ @SuppressWarnings("restriction") public class LogContent<T extends IModelNode> extends Composite { /** * Re-use the existing functionality for generating hyperlinks in stack traces * in console output. Main difference is that we explicitly specify the URL, * whereas our superclass extracts it from the console document. */ private static class DevModeStackTraceHyperlink extends JavaStackTraceHyperlink { private final String url; public DevModeStackTraceHyperlink(String url, TextConsole console) { super(console); assert (url.startsWith(JAVA_SOURCE_URL_PREFIX)); url = url.substring(JAVA_SOURCE_URL_PREFIX.length()); try { url = URLDecoder.decode(url, "UTF-8"); } catch (UnsupportedEncodingException e) { // Should never happen, but if it did, then presumably encoding failed // as well, so ignore } this.url = url; } @Override protected String getLinkText() { return url; } } /** * Matches GWT TreeLogger messages with a reference to a Java source file. * * Regex groups: 1) Absolute filesystem path to Java source. Its format can be * passed directly to the {@link Path#Path(String)} constructor. * * Example: Errors in 'file:/eclipse/Hello/src/com/example/client/Gwt.java' */ private static final Pattern GWT_ERROR_FILE_REGEX = Pattern.compile(".*'file:(.+\\.java)'"); /** * Matches GWT TreeLogger messages with a reference to a line number. These * errors are nested below errors referencing a file (if the file is Java, the * parent error should match {@link LogContent#GWT_ERROR_FILE_REGEX}. * * Regex groups: 1) The part of the line that should be enclosed in a * hyperlink, 2) The line number, and 3) The rest of the line, which is not * part of the link. * * Example: Line 22: The type Gwt must implement the inherited abstract method * EntryPoint.onModuleLoad() */ private static final Pattern GWT_ERROR_LINE_REGEX = Pattern.compile("^(Line (\\d+))(:.*)"); /** * Made-up URL prefix for an address to a line in Java source code. We define * this to differentiate these types of links from external HTTP links. */ private static final String JAVA_SOURCE_URL_PREFIX = "java://"; /** * Matches a line in a Java stack trace. * * Regex groups: 1) the URL (minus the prefix) to the Java source location, * and 2) the part of the line that should be hyperlinked. * * Example: at com.example.client.Gwt.onModuleLoad(Gwt.java:50) */ private static final Pattern JAVA_STACK_FRAME_REGEX = Pattern.compile("^\\s*at ([^\\(]+\\((.+\\.java:\\d+)\\))$"); /** * Returns the HTML markup for a hyperlink to a line in a Java source file. * * @param javaSourceAddress the location to the Java source line, specified in * the following format: com.example.Class.method(Class.java) * @param text the hyperlink text */ private static String buildJavaSourceHyperlink(String javaSourceAddress, String text) { if (javaSourceAddress == null) { // Shouldn't happen, but if we don't have a source address, just return // the text by itself (without a link). return convertToHtmlContent(text); } StringBuffer buf = new StringBuffer(); buf.append("<a href=\""); buf.append(JAVA_SOURCE_URL_PREFIX); try { buf.append(URLEncoder.encode(javaSourceAddress, "UTF-8")); } catch (UnsupportedEncodingException e) { // If we fail, just use the un-encoded URL buf.append(javaSourceAddress); } buf.append("\">"); buf.append(convertToHtmlContent(text)); buf.append("</a>"); return buf.toString(); } private static String collapseHtmlFormatting(String html) { html = html.replace("<br/>", "\n"); html = html.replaceAll("<[^<>]+>", ""); return html; } /** * Computes the address of a particular line in a Java source file, in the * format accepted by * {@link LogContent#buildJavaSourceHyperlink(String, String)}. */ private static String computeJavaSourceAddress(String filesystemPath, String lineNumber) { IPath path = new Path(filesystemPath); IFile file = ResourcesPlugin.getWorkspace().getRoot().getFileForLocation( path); if (file != null && file.exists()) { ICompilationUnit cu = JavaCore.createCompilationUnitFrom(file); if (cu != null) { IJavaElement cuParent = cu.getParent(); if (cuParent instanceof IPackageFragment) { IPackageFragment pckgFragment = (IPackageFragment) cuParent; StringBuffer sb = new StringBuffer(); sb.append(pckgFragment.getElementName()); /* * We can use anything for the type and method, since * JavaStackTraceHyperlink will strip it out anyway (it only uses the * package name, the compilation unit name, and the line number). */ sb.append(".DummyType.dummyMethod"); sb.append('('); sb.append(cu.getElementName()); sb.append(':'); sb.append(lineNumber); sb.append(')'); return sb.toString(); } } } return null; } private static String convertToHtmlContent(String unescapedStr) { Java2HTMLEntityReader reader = new Java2HTMLEntityReader(new StringReader( unescapedStr)); try { String escapedString = reader.getString(); /* * NOTE: The expansion of '\t' into four spaces does not technically * belong here. We are assuming that whatever is rendering the HTML * content will preserve whitespace. There might be a better way to deal * with this. */ escapedString = escapedString.replaceAll("\t", " "); escapedString = escapedString.replaceAll("\n", "<br/>"); // the html renderer doesn't understand ˆ or ˜ by themselves escapedString = escapedString.replaceAll("ˆ", "^"); escapedString = escapedString.replaceAll("˜", "~"); return escapedString; } catch (IOException e) { return unescapedStr; } } private final Log<T> log; private FilteredTree logEntries; private LogLabelProvider<T> logLabelProvider; private ScrolledFormText scrolledFormDetailsText; private String scrolledFormDetailsTextContents; private TreeViewer treeViewer; public LogContent(Composite parent, Log<T> log) { super(parent, SWT.NONE); this.log = log; setLayout(new FillLayout()); SashForm sashForm = new SashForm(this, SWT.VERTICAL); createViewer(sashForm); createDetailsPane(sashForm, treeViewer.getTree().getBackground()); sashForm.setWeights(new int[] {70, 30}); revealChildrenThatNeedAttention(treeViewer, log.getRootLogEntry()); LogEntry<T> entryToSelect = log.getFirstDeeplyNestedChildWithMaxAttn(); if (entryToSelect != null) { treeViewer.setSelection(new StructuredSelection(entryToSelect)); } } private String buildDetailsHtml(String content) { StringBuffer buf = new StringBuffer(); for (String line : content.split("\\n")) { // Add hyperlinks to Java stack traces Matcher matcher = JAVA_STACK_FRAME_REGEX.matcher(line); if (matcher.matches()) { buf.append(convertToHtmlContent(line.substring(0, matcher.start(2)))); buf.append(buildJavaSourceHyperlink(matcher.group(1), matcher.group(2))); buf.append(convertToHtmlContent(line.substring(matcher.end(2)))); } else { buf.append(convertToHtmlContent(line)); } buf.append("<br/>"); } return buf.toString(); } private String buildLabelHtml(LogEntry<?> logEntry) { String label = logEntry.getLogData().getLabel(); LogEntry<?> parentLogEntry = logEntry.getParent(); if (parentLogEntry != null) { Data parentData = parentLogEntry.getLogData(); if (parentData != null) { String parentLabel = parentData.getLabel(); Matcher matcher = GWT_ERROR_LINE_REGEX.matcher(label); Matcher parentMatcher = GWT_ERROR_FILE_REGEX.matcher(parentLabel); // If the log entry is a GWT error with a line number, link to the // offending line in the Java source. if (matcher.matches() && parentMatcher.matches()) { String address = computeJavaSourceAddress(parentMatcher.group(1), matcher.group(2)); if (address != null) { return buildJavaSourceHyperlink(address, matcher.group(1)) + convertToHtmlContent(matcher.group(3)); } } } } // No hyperlinks to insert return convertToHtmlContent(label); } private void copyToClipboard(String text) { Clipboard clipboard = new Clipboard(getDisplay()); TextTransfer tt = TextTransfer.getInstance(); clipboard.setContents(new Object[] {text}, new Transfer[] {tt}); } private void copyTreeSelectionToClipboard() { ITreeSelection selection = (ITreeSelection) treeViewer.getSelection(); TreePath[] paths = selection.getPaths(); StringBuffer buf = new StringBuffer(); for (TreePath path : paths) { LogEntry<?> entry = (LogEntry<?>) path.getLastSegment(); buf.append(createTabString(path.getSegmentCount() - 1)); buf.append(entry.toString()); buf.append("\n"); } if (buf.length() > 0) { buf.deleteCharAt(buf.length() - 1); // take off last \n } copyToClipboard(buf.toString()); } private void createDetailsPane(Composite parent, Color backgroundColor) { scrolledFormDetailsText = new ScrolledFormText(parent, SWT.V_SCROLL | SWT.H_SCROLL | SWT.BORDER, false); /* * If focusable, the FormText will jump to its top-most hyperlink anytime it * is forced to take focus. This surfaces when the user clicks on a * hyperlink to open a Java file; the ApplicationWindow will tell the * previously focused control to force focus, and the buffer jumps to the * top. The workaround is not make the FormText not focusable, and to * achieve this we instantiate/set it manually. This workaround has the * drawback that it can't receive keyboard events, eg ctrl-c for copy * */ scrolledFormDetailsText.setFormText(new FormText(scrolledFormDetailsText, SWT.NO_FOCUS)); scrolledFormDetailsText.setBackground(backgroundColor); Menu menu = scrolledFormDetailsText.getContent().getMenu(); MenuItem copyAllMenuItem = new MenuItem(menu, SWT.PUSH); copyAllMenuItem.setText("Copy &All"); copyAllMenuItem.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { copyToClipboard(scrolledFormDetailsTextContents); } }); FormText formText = scrolledFormDetailsText.getFormText(); // Don't collapse consecutive whitespace into a single space formText.setWhitespaceNormalized(false); formText.addHyperlinkListener(new IHyperlinkListener() { @Override public void linkActivated(HyperlinkEvent e) { String url = (String) e.getHref(); if (url.startsWith(JAVA_SOURCE_URL_PREFIX)) { openJavaSource(url); } else { openInDefaultBrowser(url); } } @Override public void linkEntered(HyperlinkEvent e) { // Ignore } @Override public void linkExited(HyperlinkEvent e) { // Ignore } }); } private String createTabString(int n) { if (n == 0) { return ""; } StringBuffer b = new StringBuffer(); for (int i = 0; i < n; i++) { b.append("\t"); } return b.toString(); } @SuppressWarnings("deprecation") private void createViewer(Composite parent) { logEntries = new FilteredTree(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL | SWT.BORDER, new PatternFilter()); treeViewer = logEntries.getViewer(); treeViewer.setComparator(new ViewerComparator() { @Override public int compare(Viewer viewer, Object e1, Object e2) { assert (e1 instanceof LogEntry<?>); assert (e2 instanceof LogEntry<?>); LogEntry<?> entry1 = (LogEntry<?>) e1; LogEntry<?> entry2 = (LogEntry<?>) e2; assert (entry2.getParent() == entry1.getParent()); List<?> siblings = entry1.getParent().getAllChildren(); return (siblings.indexOf(entry1) - siblings.indexOf(entry2)); } }); logLabelProvider = new LogLabelProvider<T>(); treeViewer.setLabelProvider(logLabelProvider); treeViewer.setContentProvider(new LogContentProvider<T>()); treeViewer.setInput(log.getRootLogEntry()); treeViewer.addSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent event) { updateDetailsPane(event); } }); treeViewer.getTree().addKeyListener( new EnterKeyTreeToggleKeyAdapter(treeViewer)); Menu menu = new Menu(treeViewer.getTree()); MenuItem copy = new MenuItem(menu, SWT.NONE); copy.setText("Copy"); copy.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { copyTreeSelectionToClipboard(); } }); new MenuItem(menu, SWT.SEPARATOR); MenuItem collapseAll = new MenuItem(menu, SWT.NONE); collapseAll.setText("Collapse All"); collapseAll.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { treeViewer.collapseAll(); } }); MenuItem expandAll = new MenuItem(menu, SWT.NONE); expandAll.setText("Expand All"); expandAll.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { treeViewer.expandAll(); } }); treeViewer.getTree().setMenu(menu); treeViewer.getTree().addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { // SWT.MOD1 corresponds to the ctrl key on Windows/linux, and command on // mac if ((e.stateMask & SWT.MOD1) > 0 && e.keyCode == 'c') { copyTreeSelectionToClipboard(); } } }); } /** * Find the TextConsole associated with the launch. This is required by the * {@link JavaStackTraceHyperlink} class (which we subclass). */ private TextConsole getLaunchConsole() { LaunchConfiguration launchConfiguration = null; T entity = log.getEntity(); if (entity instanceof BrowserTab) { BrowserTab browserTab = (BrowserTab) entity; launchConfiguration = browserTab.getLaunchConfiguration(); } else if (entity instanceof LaunchConfiguration) { launchConfiguration = (LaunchConfiguration) entity; } if (launchConfiguration != null) { IProcess[] processes = launchConfiguration.getLaunch().getProcesses(); if (processes.length > 0) { /* * Just get the console for the first process. If there are multiple * processes, they will all link back to the same ILaunch (which is what * JavaStackTraceHyperlink uses the console for anyway). */ IConsole console = DebugUITools.getConsole(processes[0]); if (console instanceof TextConsole) { return (TextConsole) console; } } } return null; } private void openInDefaultBrowser(String url) { BrowserUtilities.launchBrowserAndHandleExceptions(url); } private void openJavaSource(String url) { TextConsole console = getLaunchConsole(); if (console != null) { new DevModeStackTraceHyperlink(url, console).linkActivated(); } else { MessageDialog.openInformation(getShell(), "GWT Eclipse Plugin", "Could not find Java source context."); } } /** * Reveal all children in the model that require attention. */ private void revealChildrenThatNeedAttention(TreeViewer viewer, LogEntry<T> entry) { Data logData = entry.getLogData(); if (logData != null && logData.getNeedsAttention()) { viewer.reveal(entry); } List<LogEntry<T>> disclosedChildren = entry.getDisclosedChildren(); for (LogEntry<T> logEntry : disclosedChildren) { revealChildrenThatNeedAttention(viewer, logEntry); } } private void updateDetailsPane(SelectionChangedEvent event) { StructuredSelection structuredSelection = (StructuredSelection) event.getSelection(); if (structuredSelection == null || structuredSelection.isEmpty()) { scrolledFormDetailsText.setText(""); return; } LogEntry<?> logEntry = (LogEntry<?>) (structuredSelection.getFirstElement()); Color foregroundColor = logLabelProvider.getForeground(logEntry); scrolledFormDetailsText.setForeground(foregroundColor); LogEntry.Data data = logEntry.getLogData(); StringBuffer buf = new StringBuffer(); buf.append("<p>"); Date logEntryDate = new Date(data.getTimestamp()); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss.SSS"); buf.append(simpleDateFormat.format(logEntryDate)); // Add the log level // FIXME: Need coloring here, for error entries. buf.append(" ["); buf.append(logEntry.getLogData().getLogLevel()); buf.append("]"); // Add the module name buf.append(" ["); buf.append(logEntry.getModuleHandle().getName()); buf.append("] "); buf.append(buildLabelHtml(logEntry)); buf.append("<br/>"); // Add the detailed information, if available String details = buildDetailsHtml(logEntry.getLogData().getDetails()); if (details.length() > 0) { buf.append("<br/>"); buf.append(details); buf.append("<br/>"); } /* * Add help information, if available. */ String helpInfoURL = logEntry.getLogData().getHelpInfoURL(); if (helpInfoURL.length() > 0) { String escapedHelpInfoURL = convertToHtmlContent(helpInfoURL); buf.append("<br/>"); buf.append("See the following URL for additional information:<br/>"); buf.append("<br/>"); buf.append("<a href='"); buf.append(escapedHelpInfoURL); buf.append("'>"); buf.append(escapedHelpInfoURL); buf.append("</a>"); buf.append("<br/>"); } else { /* * TODO: We always defer to the Help Info URL, as the Help Info text is in * HTML, and we're not rendering HTML in this region as yet. */ } buf.append("</p>"); String text = buf.toString(); scrolledFormDetailsTextContents = collapseHtmlFormatting(text); scrolledFormDetailsText.setText(text); } }