/* FeatureIDE - A Framework for Feature-Oriented Software Development * Copyright (C) 2005-2013 FeatureIDE team, University of Magdeburg, Germany * * This file is part of FeatureIDE. * * FeatureIDE 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 3 of the License, or * (at your option) any later version. * * FeatureIDE 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 FeatureIDE. If not, see <http://www.gnu.org/licenses/>. * * See http://www.fosd.de/featureide/ for further information. */ package coverplugin; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerException; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceChangeEvent; import org.eclipse.core.resources.IResourceChangeListener; import org.eclipse.core.resources.IResourceDelta; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.NullProgressMonitor; 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.ui.IEditorInput; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IPropertyListener; import org.eclipse.ui.part.FileEditorInput; import org.eclipse.ui.texteditor.IDocumentProvider; import org.eclipse.ui.texteditor.ITextEditor; import org.xml.sax.SAXException; import coverage.Coverage; import coverage.Interaction; import coverage.UnsupportedCoverageException; import coverage.XMLReader; import coverplugin.LOGGER.COLOR; /** * Assigns color annotations to the editor. * * @author Jens Meinicke */ public final class ColorAnnotationModel implements IAnnotationModel { /** Key used to piggyback the model to the editors model. */ private static final Object KEY = new Object(); private static boolean highlighting = true; /** List of current ColorAnnotations */ private List<ColorAnnotation> annotations = new ArrayList<ColorAnnotation>(32); private HashMap<Integer, Position> annotatedPositions = new HashMap<Integer, Position>(); /** List of registered IAnnotationModelListener */ private Set<IAnnotationModelListener> annotationModelListeners = new HashSet<IAnnotationModelListener>(2); private final IDocument document; private final IFile file; private int openConnections = 0; private int docLines, docLength; private IDocumentListener documentListener = new IDocumentListener() { @Override public void documentChanged(DocumentEvent event) { IDocument newDoc = event.getDocument(); if (docLines != newDoc.getNumberOfLines()) { updateAnnotations(false); docLines = newDoc.getNumberOfLines(); docLength = newDoc.getLength(); } else { changeAnnotations(event.getOffset(), newDoc.getLength()); } } @Override public void documentAboutToBeChanged(DocumentEvent event) { } }; private IResourceChangeListener listener = new IResourceChangeListener() { @Override public void resourceChanged(IResourceChangeEvent event) { IResourceDelta[] deltas = event.getDelta().getAffectedChildren(); for (IResourceDelta rd :deltas) { for (IResourceDelta child : rd.getAffectedChildren()) { IResource res = child.getResource(); if (res instanceof IFile) { if (res.getName().equals("coverage.xml")) { updateAnnotations(true); } } } } } }; boolean POLLING= true; private Thread pollThread = new Thread() { public void run() { while (POLLING) { try { file.getProject().getFile("coverage.xml").refreshLocal(IResource.DEPTH_ZERO, new NullProgressMonitor()); } catch (CoreException e) { e.printStackTrace(); } synchronized (this) { try { wait(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }; }; private ColorAnnotationModel(IDocument document, IFile file, ITextEditor editor) { ResourcesPlugin.getWorkspace().addResourceChangeListener(listener); this.document = document; this.file = file; docLines = document.getNumberOfLines(); docLength = document.getLength(); updateAnnotations(true); editor.addPropertyListener(new IPropertyListener() { @Override public void propertyChanged(Object source, int propId) { if (propId == IEditorPart.PROP_DIRTY && !((ITextEditor) source).isDirty()) { updateAnnotations(true); } } }); pollThread.start(); } /** * 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 attach a annotation model to */ public static boolean attach(ITextEditor editor) { IDocumentProvider provider = editor.getDocumentProvider(); IEditorInput input = editor.getEditorInput(); if (provider != null && (input instanceof FileEditorInput)) { IAnnotationModel model = provider.getAnnotationModel(input); if (model instanceof IAnnotationModelExtension) { IAnnotationModelExtension modelex = (IAnnotationModelExtension) model; ColorAnnotationModel colormodel = (ColorAnnotationModel) modelex.getAnnotationModel(KEY); if (colormodel == null) { IFile file = ((FileEditorInput) input).getFile(); IDocument document = provider.getDocument(input); colormodel = new ColorAnnotationModel(document, file, editor); modelex.addAnnotationModel(KEY, colormodel); return true; } } } return false; } /** * Detaches the coverage annotation model from the given editor. If the * editor does not have a model attached, this method does nothing. * * @param editor * Editor to detach the annotation model from */ public static void detach(ITextEditor editor) { IDocumentProvider provider = editor.getDocumentProvider(); if (provider != null) { IAnnotationModel model = provider.getAnnotationModel(editor.getEditorInput()); if (model instanceof IAnnotationModelExtension) { IAnnotationModelExtension modelex = (IAnnotationModelExtension) model; modelex.removeAnnotationModel(KEY); } } } /** * Changes the whether the editor highlights the directives or not. * * Updates the annotations if the value changed. * * @param highlighting * true: highlights directives in the editor */ public static void setHighlighting(boolean highlighting, ITextEditor editor) { if (ColorAnnotationModel.highlighting != highlighting) { ColorAnnotationModel.highlighting = highlighting; attach(editor); } } /** * This method is called, when the document is changed, but the number of * lines stays the same. * * It updates the offset and length of annotations, with an offset greater * than the "change offset". * * @param offset * the change offset * @param newLength * the length of the changed document */ private void changeAnnotations(int offset, int newLength) { AnnotationModelEvent modelEvent = new AnnotationModelEvent(this); for (ColorAnnotation annotation : annotations) { Position pos = annotation.getPosition(); if (pos.getOffset() > offset) { annotation.updateOffset(newLength - docLength); modelEvent.annotationChanged(annotation); } else if (pos.includes(offset)) { annotation.updateLength(newLength - docLength); modelEvent.annotationChanged(annotation); } } docLength = newLength; fireModelChanged(modelEvent); } /** * This method is called, when the document is saved or when the document * and the number of lines are changed. * * It removes all annotations and creates new. * * @param createNew * true: builds new FSTModel false: only gets new FSTDirectives */ private void updateAnnotations(boolean createNew) { if (!annotations.isEmpty()) { clear(); } // if (createNew) { annotatedPositions = new HashMap<Integer, Position>(docLines); // createDirectiveList(); createAnnotations(); // } else if (!directiveMap.isEmpty()) { // annotatedPositions.clear(); // // updateDirectives(); // createAnnotations(); // } } /** * Removes all annotations. */ private void clear() { AnnotationModelEvent event = new AnnotationModelEvent(this); for (final ColorAnnotation ca : annotations) { event.annotationRemoved(ca, ca.getPosition()); } annotations.clear(); fireModelChanged(event); } IFile coveragIFile; /** * Creates the color annotations from the FSTDirectives. */ private void createAnnotations() { LOGGER.log(COLOR.BLUE, "ColorAnnotationModel.createAnnotations for " + file); List<Integer> lineLength = new LinkedList<Integer>(); try { InputStream inputStream = new FileInputStream(file.getRawLocation().makeAbsolute().toFile()); try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { String line; while ((line = reader.readLine()) != null) { lineLength.add(line.length()); } } AnnotationModelEvent event = new AnnotationModelEvent(this); IFile newCoveragIFile = file.getProject().getFile("coverage.xml"); if (newCoveragIFile.exists()) { coveragIFile = newCoveragIFile; } if (coveragIFile == null) { return; } LOGGER.log(COLOR.MAGENTA, coveragIFile.getFullPath()); File coveragFile = coveragIFile.getRawLocation().makeAbsolute().toFile(); Coverage coverage = new XMLReader().readFromFile(coveragFile); Collection<Interaction> coveredLines = coverage.getCoverage(file.getName()); if (coveredLines.isEmpty()) { return; } int i = 0; int from = Integer.MIN_VALUE; int length = 0; int current = Integer.MIN_VALUE; int lastInteraction = Integer.MIN_VALUE; ColorAnnotation.base = coverage.getBaseValue(); String text = ""; for (Interaction interaction : coveredLines) { int covered = interaction.getLine(); int lineNr = covered == 0 ? 0 : covered - 1; if (from == Integer.MIN_VALUE) { from = covered; current = covered; lastInteraction = interaction.getInteraction(); text = interaction.getText(); length = document.getLineLength(lineNr); continue; } if (covered == current + 1 && lastInteraction == interaction.getInteraction() && text.equals(interaction.getText())) { current = covered; length += document.getLineLength(lineNr); continue; } // paint last int offset = document.getLineOffset(from == 0 ? 0 : from - 1); Position newPos = new Position(offset, length); final ColorAnnotation ca; ca = new ColorAnnotation(lastInteraction, newPos, coverage.getType(), text); annotations.add(ca); event.annotationAdded(ca); annotatedPositions.put(i++, newPos); // set new start from = covered; current = covered; lastInteraction = interaction.getInteraction(); text = interaction.getText(); length = document.getLineLength(lineNr); } int offset = document.getLineOffset(from <= 0 ? 0 : from - 1); Position newPos = new Position(offset, length); ColorAnnotation ca = new ColorAnnotation(lastInteraction, newPos, coverage.getType(), text); annotations.add(ca); event.annotationAdded(ca); annotatedPositions.put(i++, newPos); fireModelChanged(event); } catch (IOException | BadLocationException | ParserConfigurationException | TransformerException | SAXException e) { e.printStackTrace(); } catch (UnsupportedCoverageException e) { // TODO Auto-generated catch block e.printStackTrace(); } } 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 addAnnotationModelListener(IAnnotationModelListener listener) { if (!annotationModelListeners.contains(listener)) { annotationModelListeners.add(listener); fireModelChanged(new AnnotationModelEvent(this, true)); } } @Override public void removeAnnotationModelListener(IAnnotationModelListener listener) { annotationModelListeners.remove(listener); } @Override public void connect(IDocument document) { if (this.document != document) throw new RuntimeException("Can't connect to different document."); for (final ColorAnnotation ca : annotations) { try { document.addPosition(ca.getPosition()); } catch (BadLocationException ex) { } } if (openConnections++ == 0) { document.addDocumentListener(documentListener); } } @Override public void disconnect(IDocument document) { if (this.document != document) throw new RuntimeException("Can't disconnect from different document."); for (final ColorAnnotation ca : annotations) { document.removePosition(ca.getPosition()); } if (--openConnections == 0) { document.removeDocumentListener(documentListener); } POLLING = false; } /** * External modification is not supported. */ @Override public void addAnnotation(Annotation annotation, Position position) { throw new UnsupportedOperationException(); } /** * External modification is not supported. */ @Override public void removeAnnotation(Annotation annotation) { throw new UnsupportedOperationException(); } @SuppressWarnings({ "rawtypes", "unchecked" }) @Override public Iterator getAnnotationIterator() { return annotations.iterator(); } @Override public Position getPosition(Annotation annotation) { if (annotation instanceof ColorAnnotation) { return ((ColorAnnotation) annotation).getPosition(); } else { return null; } } }