/*******************************************************************************
* Copyright (c) 2014-2015 Red Hat, 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:
* Red Hat Inc. - initial API and implementation
*******************************************************************************/
package org.eclipse.linuxtools.internal.gcov.view.annotatedsource;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import org.eclipse.cdt.core.model.CoreModel;
import org.eclipse.cdt.core.model.IBinary;
import org.eclipse.cdt.core.model.ICElement;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.ui.CDTUITools;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceProxy;
import org.eclipse.core.resources.IResourceProxyVisitor;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.AnnotationModelEvent;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.IAnnotationModelExtension;
import org.eclipse.jface.text.source.IAnnotationModelListener;
import org.eclipse.jface.text.source.IAnnotationModelListenerExtension;
import org.eclipse.linuxtools.binutils.link2source.STLink2SourceSupport;
import org.eclipse.linuxtools.internal.gcov.parser.CovManager;
import org.eclipse.linuxtools.internal.gcov.parser.Line;
import org.eclipse.linuxtools.internal.gcov.parser.SourceFile;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.texteditor.ITextEditor;
/**
* Annotation model responsible for management of GcovAnnotation elements.
*/
public final class GcovAnnotationModel implements IAnnotationModel {
private static final String THOROUGH_COVERAGE = "org.eclipse.linuxtools.gcov.ThoroughCoverageAnnotation"; //$NON-NLS-1$
private static final String COVERAGE = "org.eclipse.linuxtools.gcov.CoverageAnnotation"; //$NON-NLS-1$
private static final String NO_COVERAGE = "org.eclipse.linuxtools.gcov.NoCoverageAnnotation"; //$NON-NLS-1$
/** Key for identifying our model from other Editor models. */
private static final Object KEY = new Object();
/** List of GcovAnnotation elements */
private List<Annotation> annotations = new ArrayList<>();
/** List of IAnnotationModelListener */
private List<IAnnotationModelListener> annotationModelListeners = new ArrayList<>();
private final ITextEditor editor;
private final IDocument document;
private int openConnections = 0;
private boolean annotated = false;
private IDocumentListener documentListener = new IDocumentListener() {
@Override
public void documentChanged(DocumentEvent event) {
updateAnnotations(false);
}
@Override
public void documentAboutToBeChanged(DocumentEvent event) {}
};
private GcovAnnotationModel(ITextEditor editor, IDocument document) {
this.editor = editor;
this.document = document;
updateAnnotations(true);
}
/**
* Attaches a coverage annotation model for the given editor if the editor
* can be annotated. Does nothing if the model is already attached.
*
* @param editor Editor to which an annotation model should be attached
*/
public static void attach(ITextEditor editor) {
IDocumentProvider provider = editor.getDocumentProvider();
if (provider == null) {
return;
}
IAnnotationModel model = provider.getAnnotationModel(editor.getEditorInput());
if (!(model instanceof IAnnotationModelExtension)) {
return;
}
IAnnotationModelExtension modelex = (IAnnotationModelExtension) model;
IDocument document = provider.getDocument(editor.getEditorInput());
GcovAnnotationModel coveragemodel = (GcovAnnotationModel) modelex.getAnnotationModel(KEY);
if (coveragemodel == null) {
coveragemodel = new GcovAnnotationModel(editor, document);
modelex.addAnnotationModel(KEY, coveragemodel);
} else {
coveragemodel.updateAnnotations(false);
}
}
public static void clear (ITextEditor editor) {
IDocumentProvider provider = editor.getDocumentProvider();
if (provider == null) {
return;
}
IAnnotationModel model = provider.getAnnotationModel(editor.getEditorInput());
if (!(model instanceof IAnnotationModelExtension)) {
return;
}
IAnnotationModelExtension modelex = (IAnnotationModelExtension) model;
IAnnotationModel coverageModel = modelex.getAnnotationModel(KEY);
if (coverageModel instanceof GcovAnnotationModel) {
((GcovAnnotationModel) coverageModel).clear();
}
}
private void updateAnnotations(boolean force) {
// We used to not annotate any editor displaying content of an element whose project was not tracked.
// This logic fails when we have a linked-in file which won't point back to a project that has
// been registered so it has been removed.
SourceFile coverage = findSourceCoverageForEditor();
if (coverage != null) {
if (!annotated || force) {
createAnnotations(coverage);
}
} else {
if (annotated) {
clear();
}
}
}
private SourceFile findSourceCoverageForEditor() {
if (editor.isDirty()) {
return null;
}
final IEditorInput input = editor.getEditorInput();
if (input == null) {
return null;
}
ICElement element = CDTUITools.getEditorInputCElement(input);
if (element == null) {
return null;
}
return findSourceCoverageForElement(element);
}
// Private resource proxy visitor to run through a project's resources to see if
// it contains a link to a C element's resource. This allows us to locate the
// project (and it's binary) that has gcov data for a particular resource that has been linked into
// the project. We can't just query the resource for it's project in such a case. This
// is part of the fix for bug: 447554
private class FindLinkedResourceVisitor implements IResourceProxyVisitor {
final private ICElement element;
private boolean keepSearching = true;
private boolean found;
private IResource resource;
private String lastLinkPath;
public FindLinkedResourceVisitor(ICElement element) {
this.element = element;
}
public boolean foundElement() {
return found;
}
public IResource getResource() {
return resource;
}
@Override
public boolean visit(IResourceProxy proxy) {
// To correctly find a file in a linked directory, we cannot just look at the isLinked() attribute
// which is not set for the file but is set for one of its parent directories. So, we keep track
// of linked directories and use them to determine if we should bother getting the resource to compare with.
if (proxy.isLinked()) {
lastLinkPath = proxy.requestFullPath().toString();
}
if (lastLinkPath != null && proxy.requestFullPath().toString().startsWith(lastLinkPath) && proxy.requestResource().getLocationURI().equals(element.getLocationURI())) {
found = true;
resource = proxy.requestResource();
keepSearching = false;
}
return keepSearching;
}
}
private SourceFile findSourceCoverageForElement(ICElement element) {
List<SourceFile> sources = new ArrayList<> ();
ICProject cProject = element.getCProject();
IResource elementResource = element.getResource();
IPath target = GcovAnnotationModelTracker.getInstance().getBinaryPath(cProject.getProject());
if (target == null) {
// We cannot find a target for this element, using it's project.
// This can be caused by linking in a file to the project which may
// not have a project or may point to another unseen project if the file originated
// there.
IProject[] trackedProjects = GcovAnnotationModelTracker.getInstance().getTrackedProjects();
for (IProject proj : trackedProjects) {
// Look at all projects that are registered for gcov viewing and see if the
// element is linked in.
try {
FindLinkedResourceVisitor visitor = new FindLinkedResourceVisitor(element);
proj.accept(visitor, IResource.DEPTH_INFINITE);
// If we find a match, make note of the target and the real C project.
if (visitor.foundElement()) {
target = GcovAnnotationModelTracker.getInstance().getBinaryPath(proj);
cProject = CoreModel.getDefault().getCModel().getCProject(proj.getName());
elementResource = visitor.getResource();
break;
}
} catch (CoreException e) {
}
}
if (target == null) {
return null;
}
}
try {
IBinary[] binaries = cProject.getBinaryContainer().getBinaries();
for (IBinary b : binaries) {
if (b.getResource().getLocation().equals(target)) {
CovManager covManager = new CovManager(b.getResource().getLocation().toOSString());
covManager.processCovFiles(covManager.getGCDALocations(), null);
sources.addAll(covManager.getAllSrcs());
}
}
} catch (IOException|CoreException|InterruptedException e) {
}
if (elementResource != null) {
IPath elementLocation = elementResource.getLocation();
if (elementLocation != null) {
for (SourceFile sf : sources) {
IPath sfPath = new Path(sf.getName());
IFile file = STLink2SourceSupport.getFileForPath(sfPath, cProject.getProject());
if (file != null && elementLocation.equals(file.getLocation())) {
return sf;
}
// No match up to here...see if we have a relative path (Windows) to the
// source file from the binary in which case check if creating the relative
// location results in an existing file that matches one of the
// the sources. Fixes Bug 447554
if (!sfPath.isAbsolute()) {
sfPath = target.removeLastSegments(1).append(sf.getName());
if (elementLocation.equals(sfPath.makeAbsolute()) &&
sfPath.toFile().exists())
return sf;
}
}
}
}
URI elementURI = element.getLocationURI();
if (elementURI != null) {
IPath binFolder = target.removeLastSegments(1);
for (SourceFile sf : sources) {
String sfPath = Paths.get(binFolder.toOSString()).resolve(sf.getName()).normalize().toString();
if (sfPath.equals(elementURI.getPath())) {
return sf;
}
}
}
return null;
}
private void createAnnotations(SourceFile sourceFile) {
AnnotationModelEvent event = new AnnotationModelEvent(this);
clear(event);
List<Line> lines = sourceFile.getLines();
List<Long> tmp = new ArrayList<>();
for (Line line : lines) {
// Remove 0 from our calculation
if (line.getCount() != 0) {
tmp.add(line.getCount());
}
}
Long[] counts = tmp.toArray(new Long[0]);
Arrays.sort(counts);
float outlierThreshold = 0;
if (!tmp.isEmpty()) {
// Formula for outlier (upper quartile)
final int q1 = (int) Math.floor(0.25 * counts.length);
final int q3 = (int) Math.floor(0.75 * counts.length);
outlierThreshold = counts[q3] + (1.5f * (counts[q3] - counts[q1]));
}
for (int i = 0; i < lines.size(); i++) {
try {
Line line = lines.get((i+1) % lines.size());
String type = COVERAGE;
if (line.getCount() == 0) {
type = NO_COVERAGE;
} else if (line.getCount() > outlierThreshold) {
type = THOROUGH_COVERAGE;
}
if (line.exists()) {
GcovAnnotation ca = new GcovAnnotation(document.getLineOffset(i),
document.getLineLength(i), line.getCount(), type);
annotations.add(ca);
event.annotationAdded(ca);
}
} catch (BadLocationException e) {
}
}
fireModelChanged(event);
annotated = true;
}
private void clear() {
AnnotationModelEvent event = new AnnotationModelEvent(this);
clear(event);
fireModelChanged(event);
annotated = false;
}
private void clear(AnnotationModelEvent event) {
for (final Annotation ca : annotations) {
event.annotationRemoved(ca, ((GcovAnnotation)ca).getPosition());
}
annotations.clear();
}
@Override
public void addAnnotationModelListener(IAnnotationModelListener listener) {
if (!annotationModelListeners.contains(listener)) {
annotationModelListeners.add(listener);
fireModelChanged(new AnnotationModelEvent(this, true));
}
}
@Override
public void removeAnnotationModelListener(IAnnotationModelListener listener) {
annotationModelListeners.remove(listener);
}
private void fireModelChanged(AnnotationModelEvent event) {
event.markSealed();
if (!event.isEmpty()) {
for (final IAnnotationModelListener l : annotationModelListeners) {
if (l instanceof IAnnotationModelListenerExtension) {
((IAnnotationModelListenerExtension) l).modelChanged(event);
} else {
l.modelChanged(this);
}
}
}
}
@Override
public void connect(IDocument document) {
if (this.document != document) {
throw new IllegalArgumentException("Can't connect to different document."); //$NON-NLS-1$
}
for (final Annotation ca : annotations) {
try {
document.addPosition(((GcovAnnotation)ca).getPosition());
} catch (BadLocationException ex) {
}
}
if (openConnections++ == 0) {
document.addDocumentListener(documentListener);
}
}
@Override
public void disconnect(IDocument document) {
if (this.document != document) {
throw new IllegalArgumentException("Can't disconnect from different document."); //$NON-NLS-1$
}
for (final Annotation ca : annotations) {
if (ca instanceof GcovAnnotation) {
document.removePosition(((GcovAnnotation) ca).getPosition());
}
}
if (--openConnections == 0) {
document.removeDocumentListener(documentListener);
}
}
@Override
public Position getPosition(Annotation annotation) {
return (annotation instanceof GcovAnnotation) ? ((GcovAnnotation) annotation).getPosition() : null;
}
@Override
public Iterator<Annotation> getAnnotationIterator() {
return annotations.iterator();
}
@Override
public void addAnnotation(Annotation annotation, Position position) {}
@Override
public void removeAnnotation(Annotation annotation) {}
}