/*******************************************************************************
* Copyright 2005-2007, CHISEL Group, University of Victoria, Victoria, BC, Canada
* and IBM Corporation. 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:
* The Chisel Group, University of Victoria
*******************************************************************************/
package net.sourceforge.tagsea.parsed.core.internal;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.TreeSet;
import net.sourceforge.tagsea.TagSEAPlugin;
import net.sourceforge.tagsea.core.IWaypoint;
import net.sourceforge.tagsea.core.IWaypointChangeEvent;
import net.sourceforge.tagsea.core.WaypointDelta;
import net.sourceforge.tagsea.parsed.IParsedWaypointAttributes;
import net.sourceforge.tagsea.parsed.ParsedWaypointPlugin;
import net.sourceforge.tagsea.parsed.core.IParsedWaypointDefinition;
import net.sourceforge.tagsea.parsed.core.internal.WaypointParsingTools.RegionGraph;
import net.sourceforge.tagsea.parsed.core.internal.operations.AbstractWaypointUpdateOperation;
import net.sourceforge.tagsea.parsed.core.internal.operations.CleanFilesOperation;
import net.sourceforge.tagsea.parsed.core.internal.resources.DocumentRegistry;
import net.sourceforge.tagsea.parsed.parser.IMutableParsedWaypointDescriptor;
import net.sourceforge.tagsea.parsed.parser.IWaypointParseProblemCollector;
import net.sourceforge.tagsea.parsed.parser.IWaypointRefactoring;
import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.ITextFileBuffer;
import org.eclipse.core.filebuffers.LocationKind;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.MultiRule;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.swt.widgets.Display;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
/**
* Generates text changes and applies them for refactoring.
* @author Del Myers
*
*/
public class RefactoringSupport {
public static void applyWaypointChanges(WaypointDelta delta) {
HashMap<IWaypoint, List<IWaypointChangeEvent>> waypointChanges =
new HashMap<IWaypoint, List<IWaypointChangeEvent>>();
HashMap<IFile, IDocument> fileDocumentMap = new HashMap<IFile, IDocument>();
//sort the changes by waypoints and by files.
for (IWaypointChangeEvent event : delta.changes) {
List<IWaypointChangeEvent> waypointEvents = waypointChanges.get(event.getWaypoint());
if (waypointEvents == null) {
waypointEvents = new ArrayList<IWaypointChangeEvent>();
waypointChanges.put(event.getWaypoint(), waypointEvents);
}
waypointEvents.add(event);
}
//connect all the documents.
final HashMap<IFile, ITextFileBuffer> connectedMap = new HashMap<IFile, ITextFileBuffer>();
for (IWaypoint wp : waypointChanges.keySet()) {
IFile file = ParsedWaypointPlugin.getDefault().getParsedWaypointRegistry().getFileForWaypoint(wp);
if (!fileDocumentMap.containsKey(file)) {
try {
ITextFileBuffer buffer = FileBuffers.getTextFileBufferManager().getTextFileBuffer(file.getFullPath(), LocationKind.IFILE);
if (buffer == null) {
FileBuffers.getTextFileBufferManager().connect(file.getFullPath(), LocationKind.IFILE, new NullProgressMonitor());
buffer = FileBuffers.getTextFileBufferManager().getTextFileBuffer(file.getFullPath(), LocationKind.IFILE);
connectedMap.put(file, buffer);
}
IDocument document = buffer.getDocument();
fileDocumentMap.put(file, document);
} catch (CoreException e) {
ParsedWaypointPlugin.getDefault().log(e);
}
}
}
generateEdits(waypointChanges, fileDocumentMap);
//create a refresh job for all the waypoint files that weren't saved.
List<IFile> refreshList = new LinkedList<IFile>();
for (IFile file: fileDocumentMap.keySet()) {
if (!connectedMap.keySet().contains(file)) {
refreshList.add(file);
}
}
final CleanFilesOperation cleanOp = new CleanFilesOperation(refreshList);
TagSEAPlugin.run(new AbstractWaypointUpdateOperation("Refreshing changed files..."){
@Override
public IStatus run(IProgressMonitor monitor)
throws InvocationTargetException {
IStatus status = TagSEAPlugin.syncRun(cleanOp, monitor);
if (!status.isOK()) {
//refresh the whole workspace to make sure that nothing is ugly.
ParsedWaypointPlugin.getDefault().getParsedWaypointRegistry().clean(true);
}
return status;
}
@Override
public ISchedulingRule getRule() {
return cleanOp.getRule();
}
}, false);
Job commitJob = new Job("Committing changes..."){
@Override
protected IStatus run(IProgressMonitor monitor) {
monitor.beginTask("Committing waypoint refactorings....", connectedMap.keySet().size());
MultiStatus status = new MultiStatus(ParsedWaypointPlugin.PLUGIN_ID, 0, "Committing waypoint changes", null);
for (IFile file : connectedMap.keySet()) {
ITextFileBuffer buffer = connectedMap.get(file);
try {
buffer.commit(monitor, false);
file.refreshLocal(IResource.DEPTH_INFINITE, new NullProgressMonitor());
FileBuffers.getTextFileBufferManager().disconnect(file.getFullPath(), LocationKind.IFILE, new NullProgressMonitor());
} catch (CoreException e) {
status.merge(e.getStatus());
}
monitor.worked(1);
}
monitor.done();
return status;
}
};
ISchedulingRule[] schedules = new ISchedulingRule[connectedMap.keySet().size()];
int index = 0;
for (IFile file : connectedMap.keySet()) {
schedules[index] = file.getWorkspace().getRuleFactory().modifyRule(file);
index++;
}
MultiRule commitRule = new MultiRule(schedules);
commitJob.setRule(commitRule);
commitJob.schedule();
}
/**
* @param waypointChanges
* @param connectedMap
*/
private static void generateEdits(
HashMap<IWaypoint, List<IWaypointChangeEvent>> waypointChanges,
HashMap<IFile, IDocument> fileDocumentMap) {
HashMap<IFile, List<TextEdit>> edits = new HashMap<IFile, List<TextEdit>>();
for (IWaypoint wp : waypointChanges.keySet()) {
//see if any of the changes is a delete. If yes, get rid of the rest.
List<IWaypointChangeEvent> events = waypointChanges.get(wp);
for (int i = 0; i < events.size(); i++) {
if (events.get(i).getType() == IWaypointChangeEvent.DELETE) {
IWaypointChangeEvent e = events.get(i);
events.clear();
events.add(e);
}
}
IFile file = ParsedWaypointPlugin.getDefault().getParsedWaypointRegistry().getFileForWaypoint(wp);
TextEdit edit = createEdit(wp, events, file, fileDocumentMap.get(file));
if (edit != null) {
List<TextEdit> fileEdits = edits.get(file);
if (fileEdits == null) {
fileEdits = new ArrayList<TextEdit>();
edits.put(file, fileEdits);
}
fileEdits.add(edit);
}
}
applyEdits(edits, fileDocumentMap);
}
/**
* Tries to apply as many edits as possible to the document.
* @param edits
* @param fileDocumentMap
* @param connectedMap
*/
private static void applyEdits(HashMap<IFile, List<TextEdit>> edits,
HashMap<IFile, IDocument> fileDocumentMap) {
LinkedList<IFile> refreshList = new LinkedList<IFile>();
for (IFile file : edits.keySet()) {
List<TextEdit> fileEdits = edits.get(file);
//sort the edits and generate a graph of overlapping regions
//so that we can remove the minimal set of conflicts and do
//as much work as possible.
TextEdit[] editArray = fileEdits.toArray(new TextEdit[fileEdits.size()]);
IRegion[] regionArray = new IRegion[editArray.length];
for (int i = 0; i < editArray.length; i++) {
TextEdit edit = editArray[i];
regionArray[i] = new Region(edit.getOffset(), edit.getLength());
}
RegionGraph<TextEdit> overlaps = new RegionGraph<TextEdit>(regionArray, editArray);
List<TextEdit> skippedEdits = new ArrayList<TextEdit>();
while (overlaps.getOverlapCount(overlaps.peekObject()) > 0) {
skippedEdits.add(overlaps.popObject());
}
final MultiTextEdit finalEdit = new MultiTextEdit();
final IDocument document = fileDocumentMap.get(file);
finalEdit.addChildren(overlaps.getNodeObjects());
final Exception[] ex = new Exception[1];
if (Display.getCurrent() == null) {
Display.getDefault().syncExec(new Runnable(){
public void run() {
try {
//don't notify of changes that will be saved later.
((DocumentRegistry) ParsedWaypointPlugin
.getDefault().getPlatformDocumentRegistry())
.ignoreDocument(document);
finalEdit.apply(document);
((DocumentRegistry) ParsedWaypointPlugin
.getDefault().getPlatformDocumentRegistry()).watchDocument(document);
} catch (MalformedTreeException e) {
ex[0] = e;
} catch (BadLocationException e) {
ex[0] = e;
}
}});
} else {
try {
//don't notify of changes that will be saved later.
((DocumentRegistry) ParsedWaypointPlugin
.getDefault().getPlatformDocumentRegistry())
.ignoreDocument(document);
finalEdit.apply(document);
((DocumentRegistry) ParsedWaypointPlugin
.getDefault().getPlatformDocumentRegistry()).watchDocument(document);
} catch (MalformedTreeException e) {
ex[0] = e;
} catch (BadLocationException e) {
ex[0] = e;
}
}
if (ex[0] != null) {
refreshList.add(file);
}
}
if (refreshList.size() > 0) {
CleanFilesOperation cleanOp = new CleanFilesOperation(refreshList);
TagSEAPlugin.run(cleanOp, false);
}
}
/**
* @param events
* @return
*/
private static TextEdit createEdit(IWaypoint wp, List<IWaypointChangeEvent> events, IFile file, IDocument document) {
IMutableParsedWaypointDescriptor descriptor = null;
String kind = wp.getStringValue(IParsedWaypointAttributes.ATTR_KIND, null);
if (kind == null) return null;
IParsedWaypointDefinition def =
ParsedWaypointPlugin.getDefault().getParsedWaypointRegistry().getDefinition(kind);
IWaypointRefactoring refactoring = def.getRefactoringMethod();
int charStart = wp.getIntValue(IParsedWaypointAttributes.ATTR_CHAR_START, -1);
int charEnd = wp.getIntValue(IParsedWaypointAttributes.ATTR_CHAR_END, -1);
if (refactoring == null) return null;
for (IWaypointChangeEvent event : events) {
if (event.getType() == IWaypointChangeEvent.DELETE) {
TextEdit edit = refactoring.delete(wp, document);
WaypointParsingTools.deleteWaypoint(wp);
return edit;
} else {
if (descriptor == null) {
if (!event.getWaypoint().exists()) return null;
if (charStart == -1 || charEnd == -1) return null;
IWaypointParseProblemCollector collector = DocumentRegistry.INSTANCE;
IRegion r = new Region(charStart, charEnd-charStart);
((DocumentRegistry)collector).clearProblems(document, r);
descriptor = refactoring.getMutable(event.getWaypoint(), r, document, collector);
//maybe schedule the waypoint for deletion?
if (descriptor == null) return null;
}
switch (event.getType()) {
case IWaypointChangeEvent.CHANGE:
handleChangeEvent(event, descriptor);
break;
case IWaypointChangeEvent.TAG_NAME_CHANGED:
handleTagNameChangeEvent(event, descriptor);
break;
case IWaypointChangeEvent.TAGS_CHANGED:
handleTagChangeEvent(event, descriptor);
break;
}
}
}
if (descriptor != null) {
return new ReplaceEdit(charStart, charEnd-charStart, descriptor.getText());
}
return null;
}
/**
* @param event
* @param descriptor
*/
private static void handleChangeEvent(IWaypointChangeEvent event,
IMutableParsedWaypointDescriptor descriptor) {
for (String attr : event.getChangedAttributes()) {
try {
if (IParsedWaypointAttributes.ATTR_AUTHOR.equals(attr)) {
descriptor.setAuthor((String)event.getNewValue(attr));
} else if (IParsedWaypointAttributes.ATTR_DATE.equals(attr)) {
descriptor.setDate((Date)event.getNewValue(attr));
} else if (IParsedWaypointAttributes.ATTR_MESSAGE.equals(attr)) {
descriptor.setMessage((String)event.getNewValue(attr));
}
} catch (UnsupportedOperationException e) {
ParsedWaypointPlugin.getDefault().log(e);
}
}
}
/**
* @param event
* @param descriptor
*/
private static void handleTagNameChangeEvent(IWaypointChangeEvent event,
IMutableParsedWaypointDescriptor descriptor) {
try {
descriptor.replaceTag(event.getOldTagName(), event.getNewTagName());
} catch (UnsupportedOperationException e) {
ParsedWaypointPlugin.getDefault().log(e);
}
}
/**
* @param event
* @param descriptor
*/
private static void handleTagChangeEvent(IWaypointChangeEvent event,
IMutableParsedWaypointDescriptor descriptor) {
TreeSet<String> oldTags = new TreeSet<String>(Arrays.asList(event.getOldTags()));
TreeSet<String> newTags = new TreeSet<String>(Arrays.asList(event.getNewTags()));
TreeSet<String> difference = new TreeSet<String>();
difference.addAll(oldTags);
difference.removeAll(newTags);
//delete everything left in the difference
for (String tagName : difference) {
try {
descriptor.removeTag(tagName);
} catch (UnsupportedOperationException e) {
ParsedWaypointPlugin.getDefault().log(e);
}
}
difference.clear();
difference.addAll(newTags);
difference.removeAll(oldTags);
//add the new ones
for (String tagName : difference) {
try {
descriptor.addTag(tagName);
} catch(UnsupportedOperationException e) {
ParsedWaypointPlugin.getDefault().log(e);
}
}
}
}