/*
* Contributions to FindBugs
* Copyright (C) 2011, Andrey Loskutov
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package de.tobject.findbugs.view;
import java.net.MalformedURLException;
import java.net.URL;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.ui.JavaUI;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTError;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.browser.LocationEvent;
import org.eclipse.swt.browser.LocationListener;
import org.eclipse.swt.browser.OpenWindowListener;
import org.eclipse.swt.browser.WindowEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.List;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.ISelectionListener;
import org.eclipse.ui.ISelectionService;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.browser.IWebBrowser;
import org.eclipse.ui.browser.IWorkbenchBrowserSupport;
import org.eclipse.ui.forms.events.ExpansionEvent;
import org.eclipse.ui.forms.events.IExpansionListener;
import org.eclipse.ui.forms.widgets.ExpandableComposite;
import org.eclipse.ui.ide.IDE;
import de.tobject.findbugs.FindbugsPlugin;
import de.tobject.findbugs.marker.FindBugsMarker;
import de.tobject.findbugs.marker.FindBugsMarker.MarkerConfidence;
import de.tobject.findbugs.reporter.MarkerUtil;
import de.tobject.findbugs.util.EditorUtil;
import de.tobject.findbugs.util.Util;
import edu.umd.cs.findbugs.BugAnnotation;
import edu.umd.cs.findbugs.BugCategory;
import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugPattern;
import edu.umd.cs.findbugs.BugRankCategory;
import edu.umd.cs.findbugs.ClassAnnotation;
import edu.umd.cs.findbugs.DetectorFactory;
import edu.umd.cs.findbugs.DetectorFactoryCollection;
import edu.umd.cs.findbugs.FieldAnnotation;
import edu.umd.cs.findbugs.MethodAnnotation;
import edu.umd.cs.findbugs.Plugin;
import edu.umd.cs.findbugs.SourceLineAnnotation;
import edu.umd.cs.findbugs.TypeAnnotation;
import edu.umd.cs.findbugs.ba.SignatureParser;
import edu.umd.cs.findbugs.classfile.ClassDescriptor;
import edu.umd.cs.findbugs.classfile.DescriptorFactory;
import edu.umd.cs.findbugs.util.ClassName;
/**
* @author Andrei
*
*/
public class BugInfoView extends AbstractFindbugsView {
private Browser browser;
private Composite rootComposite;
private BugPattern pattern;
private String oldText;
private BugInstance bug;
private String browserId;
private volatile boolean allowUrlChange;
private List annotationList;
private IMarker marker;
private IFile file;
private IJavaElement javaElt;
private ISelectionListener selectionListener;
private IWorkbenchPart contributingPart;
private volatile boolean showingAnnotation;
private final IExpansionListener expansionListener;
public BugInfoView() {
super();
expansionListener = new IExpansionListener() {
public void expansionStateChanging(ExpansionEvent e) {
// noop
}
public void expansionStateChanged(ExpansionEvent e) {
rootComposite.layout(true, true);
rootComposite.redraw();
}
};
}
@Override
public Composite createRootControl(Composite parent) {
createRootComposite(parent);
createAnnotationList(rootComposite);
// initScrolledComposite(parent);
createBrowser(rootComposite);
// Add selection listener to detect click in problems view or bug tree
// view
ISelectionService theService = getSite().getWorkbenchWindow().getSelectionService();
selectionListener = new MarkerSelectionListener(this);
theService.addSelectionListener(selectionListener);
return rootComposite;
}
private void createRootComposite(Composite parent) {
rootComposite = new Composite(parent, SWT.NONE);
GridLayout layout = new GridLayout(1, true);
layout.marginLeft = -5;
layout.marginTop = -5;
layout.marginBottom = -5;
layout.marginRight = -5;
rootComposite.setLayout(layout);
rootComposite.setSize(SWT.DEFAULT, SWT.DEFAULT);
}
private void createBrowser(Composite parent) {
GridData data = new GridData(GridData.FILL_BOTH);
data.grabExcessHorizontalSpace = true;
data.grabExcessVerticalSpace = true;
try {
browser = new Browser(parent, SWT.NO_BACKGROUND);
browser.setLayoutData(data);
browser.setBackground(parent.getBackground());
browser.addOpenWindowListener(new OpenWindowListener() {
public void open(WindowEvent event) {
event.required = true; // Cancel opening of new windows
}
});
browser.addLocationListener(new LocationListener() {
public void changed(LocationEvent event) {
// ignore
}
public void changing(LocationEvent event) {
// fix for SWT code on Won32 platform: it uses "about:blank"
// before
// set any non-null url. We ignore this url
if (allowUrlChange || "about:blank".equals(event.location)) {
return;
}
// disallow changing of property view content
event.doit = false;
// for any external url clicked by user we should leave
// property view
openBrowserInEditor(event);
}
});
} catch (SWTError e) {
FindbugsPlugin plugin = FindbugsPlugin.getDefault();
plugin.logException(new RuntimeException(e.getMessage(), e),
"Could not create org.eclipse.swt.widgets.Composite.Browser");
}
}
private void createAnnotationList(Composite parent) {
ExpandableComposite exp = new ExpandableComposite(parent, SWT.NONE,
ExpandableComposite.TREE_NODE
| ExpandableComposite.COMPACT
| ExpandableComposite.EXPANDED
// | ExpandableComposite.NO_TITLE
// | ExpandableComposite.FOCUS_TITLE
// | ExpandableComposite.TITLE_BAR
// | ExpandableComposite.LEFT_TEXT_CLIENT_ALIGNMENT
//| ExpandableComposite.LEFT_TEXT_CLIENT_ALIGNMENT
);
exp.addExpansionListener(expansionListener);
exp.setText("Navigation");
annotationList = new List(exp, SWT.V_SCROLL | SWT.H_SCROLL | SWT.BORDER);
GridData data = new GridData(GridData.FILL_HORIZONTAL);
exp.setLayoutData(data);
exp.setClient(annotationList);
exp.setBackground(parent.getBackground());
exp.setFont(JFaceResources.getDialogFont());
annotationList.setFont(JFaceResources.getDialogFont());
annotationList.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent evnt) {
selectInEditor(false);
}
});
annotationList.addMouseListener(new MouseAdapter() {
@Override
public void mouseDoubleClick(MouseEvent e) {
selectInEditor(true);
}
});
final Menu menu = new Menu(annotationList);
final MenuItem item = new MenuItem(menu, SWT.PUSH);
item.setImage(PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_TOOL_COPY));
item.setText("Copy To Clipboard");
item.addListener(SWT.Selection, new Listener() {
public void handleEvent(Event e) {
copyInfoToClipboard();
}
});
menu.addListener(SWT.Show, new Listener() {
public void handleEvent(Event event) {
item.setEnabled(bug != null);
}
});
annotationList.setToolTipText("Click on lines or methods to go to them");
annotationList.setMenu(menu);
annotationList.pack(true);
}
private void refreshBrowser() {
String html = null;
if (browser != null && !browser.isDisposed()) {
html = getHtml();
// avoid flickering if same input
if (!html.equals(oldText)) {
allowUrlChange = true;
browser.setText(html);
allowUrlChange = false;
}
}
oldText = html;
}
private String getHtml() {
if (pattern == null) {
return "";
}
boolean hasBug = bug != null;
StringBuilder text = new StringBuilder();
if (!hasBug) {
text.append("<b>Pattern</b>: ");
text.append(pattern.getShortDescription());
} else {
text.append(pattern.getDetailText());
}
if (!hasBug) {
return text.toString();
}
if(text.lastIndexOf("</p>") == -1 || text.lastIndexOf("<br>") == -1) {
text.append("\n<p>");
}
text.append(getBugDetails());
text.append("<br>");
text.append(getPatternDetails());
addDetectorInfo(text);
String html = "<b>Bug</b>: " + toSafeHtml(bug.getMessageWithoutPrefix()) + "<br>\n" + text.toString();
return html;
}
private String getBugDetails() {
StringBuilder sb = new StringBuilder();
int rank = 0;
MarkerConfidence confidence = MarkerConfidence.Ignore;
if(bug != null) {
confidence = MarkerConfidence.getConfidence(bug.getPriority());
rank = bug.getBugRank();
} else if(marker != null) {
confidence = MarkerUtil.findConfidenceForMarker(marker);
rank = MarkerUtil.findBugRankForMarker(marker);
}
sb.append("\n<b>Rank</b>: ");
sb.append(BugRankCategory.getRank(rank));
sb.append(" (").append(rank).append(")");
sb.append(", <b>confidence</b>: ").append(confidence);
return sb.toString();
}
private String getPatternDetails() {
if (pattern == null) {
return "";
}
StringBuilder sb = new StringBuilder("<b>Pattern</b>: ");
sb.append(pattern.getType());
sb.append("\n<br><b>Type</b>: ").append(pattern.getAbbrev()).append(", <b>Category</b>: ");
sb.append(pattern.getCategory());
BugCategory category = DetectorFactoryCollection.instance().getBugCategory(pattern.getCategory());
if(category != null) {
sb.append(" (");
sb.append(category.getShortDescription());
sb.append(")");
}
return sb.toString();
}
private void addDetectorInfo(StringBuilder text) {
DetectorFactory factory = bug.getDetectorFactory();
if (factory != null) {
Plugin plugin = factory.getPlugin();
if (!plugin.isCorePlugin()) {
text.append("<p><small><i>Reported by: ").append(factory.getFullName());
text.append("<br>Contributed by plugin: ").append(plugin.getPluginId());
text.append("<br>Provider: ").append(plugin.getProvider());
String website = plugin.getWebsite();
if (website != null && website.length() > 0) {
text.append(" (<a href=\"").append(website).append("\">");
text.append(website).append("</a>)");
}
text.append("</i></small>");
}
}
}
private static String toSafeHtml(String s) {
if (s.indexOf(">") >= 0) {
s = s.replace(">", ">");
}
if (s.indexOf("<") >= 0) {
s = s.replace("<", "<");
}
return s;
}
@Override
public void dispose() {
if (selectionListener != null) {
getSite().getWorkbenchWindow().getSelectionService().removeSelectionListener(selectionListener);
selectionListener = null;
}
if (rootComposite != null && !rootComposite.isDisposed()) {
rootComposite.dispose();
}
super.dispose();
}
private void openBrowserInEditor(LocationEvent event) {
URL url;
try {
url = new URL(event.location);
} catch (MalformedURLException ignored) {
return;
}
IWorkbenchBrowserSupport support = PlatformUI.getWorkbench().getBrowserSupport();
try {
IWebBrowser newBrowser = support.createBrowser(browserId);
browserId = newBrowser.getId();
newBrowser.openURL(url);
return;
} catch (PartInitException e) {
FindbugsPlugin.getDefault().logException(e, "Can't open external browser");
}
}
private void refreshAnnotations() {
annotationList.removeAll();
// bug may be null, but if so then the error has already been logged.
if (bug != null) {
annotationList.add(bug.getMessageWithoutPrefix());
for(BugAnnotation ba : bug.getAnnotationsForMessage(false)) {
annotationList.add(ba.toString());
}
}
annotationList.pack(true);
}
private IJavaProject getIProject() {
if (javaElt != null) {
return javaElt.getJavaProject();
}
if (file != null) {
IProject p = file.getProject();
try {
if (p.hasNature(JavaCore.NATURE_ID)) {
return JavaCore.create(p);
}
} catch (CoreException e) {
FindbugsPlugin.getDefault().logException(e, "Could not open Java project for " + file);
}
}
return null;
}
private void showAnnotation(IEditorPart activeEditor) {
if (showingAnnotation) {
FindbugsPlugin.getDefault().logInfo("Recursive showAnnotation");
}
showingAnnotation = true;
try {
int index = annotationList.getSelectionIndex() - 1;
if (index >= 0) {
BugAnnotation theAnnotation = bug.getAnnotationsForMessage(false).get(index);
findLocation: try {
if (theAnnotation instanceof SourceLineAnnotation) {
SourceLineAnnotation sla = (SourceLineAnnotation) theAnnotation;
int line = sla.getStartLine();
EditorUtil.goToLine(activeEditor, line);
return;
} else if (theAnnotation instanceof MethodAnnotation) {
MethodAnnotation ma = (MethodAnnotation) theAnnotation;
String className = ma.getClassName();
IJavaProject project = getIProject();
IType type = project.findType(className);
if (type == null) {
break findLocation;
}
IMethod m = getIMethod(type, ma);
if (m != null) {
JavaUI.openInEditor(m, true, true);
} else {
activeEditor = JavaUI.openInEditor(type, true, true);
SourceLineAnnotation sla = ma.getSourceLines();
EditorUtil.goToLine(activeEditor, sla.getStartLine());
}
return;
} else if (theAnnotation instanceof FieldAnnotation) {
FieldAnnotation fa = (FieldAnnotation) theAnnotation;
String className = fa.getClassName();
IJavaProject project = getIProject();
IType type = project.findType(className);
if (type == null) {
break findLocation;
}
IField f = type.getField(fa.getFieldName());
if (f != null) {
JavaUI.openInEditor(f, true, true);
} else {
activeEditor = JavaUI.openInEditor(type, true, true);
SourceLineAnnotation sla = fa.getSourceLines();
EditorUtil.goToLine(activeEditor, sla.getStartLine());
}
return;
} else if (theAnnotation instanceof TypeAnnotation) {
TypeAnnotation fa = (TypeAnnotation) theAnnotation;
String className = ClassName.fromFieldSignature(fa.getTypeDescriptor());
if (className == null) {
break findLocation;
}
IJavaProject project = getIProject();
IType type = project.findType(ClassName.toDottedClassName(className));
if (type == null) {
break findLocation;
}
JavaUI.openInEditor(type, true, true);
return;
} else if (theAnnotation instanceof ClassAnnotation) {
ClassAnnotation fa = (ClassAnnotation) theAnnotation;
String className = fa.getClassName();
IJavaProject project = getIProject();
IType type = project.findType(className);
if (type == null) {
break findLocation;
}
JavaUI.openInEditor(type, true, true);
return;
}
} catch (JavaModelException e) {
FindbugsPlugin.getDefault().logException(e, "Could not open editor for " + theAnnotation);
} catch (PartInitException e) {
FindbugsPlugin.getDefault().logException(e, "Could not open editor for " + theAnnotation);
}
}
if(marker != null) {
int line = marker.getAttribute(IMarker.LINE_NUMBER, EditorUtil.DEFAULT_LINE_IN_EDITOR);
EditorUtil.goToLine(activeEditor, line);
}
} finally {
showingAnnotation = false;
}
}
private static String stripFirstAndLast(String s) {
return s.substring(1, s.length()-1);
}
private static IMethod getIMethod(IType type, MethodAnnotation mma) throws JavaModelException {
String name = mma.getMethodName();
SignatureParser parser = new SignatureParser(mma.getMethodSignature());
String[] arguments = parser.getArguments();
nextMethod: for(IMethod m : type.getMethods()) {
if (!m.getElementName().equals(name)) {
continue nextMethod;
}
String [] mArguments = m.getParameterTypes();
if (arguments.length != mArguments.length) {
continue nextMethod;
}
for(int i = 0; i < arguments.length; i++) {
String a = arguments[i];
String ma = mArguments[i];
while (a.startsWith("[") && ma.startsWith("[")) {
a = a.substring(1);
ma = ma.substring(1);
}
if (ma.startsWith("Q")) {
ma = stripFirstAndLast(ma);
ClassDescriptor ad = DescriptorFactory.createClassDescriptorFromFieldSignature(a);
if (ad == null) {
continue nextMethod;
}
a = ad.getSimpleName();
}
if (!ma.equals(a)) {
continue nextMethod;
}
}
return m;
}
return null;
}
private void copyInfoToClipboard() {
if(bug == null) {
return;
}
StringBuffer sb = new StringBuffer();
sb.append(removeHtmlMarkup(getHtml()));
sb.append("\n\n");
for(BugAnnotation ba : bug.getAnnotationsForMessage(true)) {
sb.append(ba.toString()).append("\n");
}
sb.append("\n");
if (file != null) {
sb.append("File: ").append(file.getLocation()).append("\n");
}
Util.copyToClipboard(sb.toString());
}
private static String removeHtmlMarkup(String html) {
// replace any amount of white space with newline between through one
// space
html = html.replaceAll("\\s*[\\n]+\\s*", " ");
// remove all valid html tags
html = html.replaceAll("<[a-zA-Z]+>", "\n");
html = html.replaceAll("</[a-zA-Z]+>", "");
// convert some of the entities which are used in current FB
// messages.xml
html = html.replaceAll(" ", "");
html = html.replaceAll("<", "<");
html = html.replaceAll(">", ">");
html = html.replaceAll("&", "&");
return html.trim();
}
private void selectInEditor(boolean openEditor) {
if (bug == null || (file == null && javaElt == null)) {
return;
}
IWorkbenchPage page = contributingPart.getSite().getPage();
IEditorPart activeEditor = page.getActiveEditor();
IEditorInput input = activeEditor != null ? activeEditor.getEditorInput() : null;
if (openEditor && !matchInput(input)) {
try {
if (file != null) {
activeEditor = IDE.openEditor(page, file);
} else if (javaElt != null) {
activeEditor = JavaUI.openInEditor(javaElt, true, true);
}
if (activeEditor != null) {
input = activeEditor.getEditorInput();
}
} catch (PartInitException e) {
FindbugsPlugin.getDefault().logException(e, "Could not open editor for " + bug.getMessage());
} catch (CoreException e) {
FindbugsPlugin.getDefault().logException(e, "Could not open editor for " + bug.getMessage());
}
}
if (matchInput(input)) {
showAnnotation(activeEditor);
}
}
private boolean matchInput(IEditorInput input) {
if (file != null && (input instanceof IFileEditorInput)) {
return file.equals(((IFileEditorInput) input).getFile());
}
if (javaElt != null && input != null) {
IJavaElement javaElement = JavaUI.getEditorInputJavaElement(input);
if (javaElt.equals(javaElement)) {
return true;
}
IJavaElement parent = javaElt.getParent();
while (parent != null && !parent.equals(javaElement)) {
parent = parent.getParent();
}
if (parent != null && parent.equals(javaElement)) {
return true;
}
}
return false;
}
private void refreshTitle() {
if (marker != null) {
String bugType = marker.getAttribute(FindBugsMarker.BUG_TYPE, "");
pattern = DetectorFactoryCollection.instance().lookupBugPattern(bugType);
}
if (pattern == null) {
return;
}
if (bug == null) {
return;
}
if(file != null) {
setContentDescription(file.getName() +
": " + marker.getAttribute(IMarker.LINE_NUMBER, 0));
} else {
setContentDescription("");
}
}
@Override
public void markerSelected(IWorkbenchPart thePart, IMarker newMarker) {
if (showingAnnotation) {
return;
}
contributingPart = thePart;
showInView(newMarker);
if (!isVisible()) {
activate();
}
}
public IWorkbenchPart getContributingPart() {
return contributingPart;
}
private void showInView(IMarker m) {
this.marker = m;
if (MarkerUtil.isFindBugsMarker(marker)) {
bug = MarkerUtil.findBugInstanceForMarker(marker);
file = (IFile) (marker.getResource() instanceof IFile ? marker.getResource() : null);
javaElt = MarkerUtil.findJavaElementForMarker(marker);
pattern = bug != null ? bug.getBugPattern() : null;
refreshTitle();
refreshAnnotations();
refreshBrowser();
rootComposite.layout(true, true);
}
}
}