/*******************************************************************************
* 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.Array;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.TreeSet;
import net.sourceforge.tagsea.TagSEAPlugin;
import net.sourceforge.tagsea.core.ITag;
import net.sourceforge.tagsea.core.IWaypoint;
import net.sourceforge.tagsea.parsed.IParsedWaypointAttributes;
import net.sourceforge.tagsea.parsed.ParsedWaypointPlugin;
import net.sourceforge.tagsea.parsed.core.ParsedWaypointUtils;
import net.sourceforge.tagsea.parsed.parser.IParsedWaypointDescriptor;
import org.eclipse.core.resources.IFile;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;
/**
*
* @author Del Myers
*
*/
public class WaypointParsingTools {
private static class RegionSorter<T> implements Comparable<RegionSorter<T>> {
private IRegion region;
private int objectId;
private T object;
private boolean isEnd;
RegionSorter(IRegion region, int objectId, T object, boolean isEnd) {
this.region = region;
this.objectId = objectId;
this.object = object;
this.isEnd = isEnd;
}
public static final Comparator<RegionSorter<?>> COMPARATOR = new Comparator<RegionSorter<?>>() {
public int compare(RegionSorter<?> o1, RegionSorter<?> o2) {
if (!o1.isEnd && o2.isEnd) {
int diff = o1.region.getOffset()-(o2.region.getOffset()+o2.region.getLength());
if (diff == 0) {
diff = o1.objectId-o2.objectId;
if (diff == 0) {
diff = 1;
}
}
return diff;
} else if (o1.isEnd && !o2.isEnd) {
int diff = (o1.region.getOffset()+o1.region.getLength())-o2.region.getOffset();
if (diff == 0) {
diff = o1.objectId-o2.objectId;
if (diff == 0) {
diff = -1;
}
}
return diff;
} else if (!(o1.isEnd || o2.isEnd)) {
int diff = o1.region.getOffset()-o2.region.getOffset();
if (diff == 0) {
return o1.objectId-o2.objectId;
}
return diff;
} else if (o1.isEnd && o2.isEnd) {
int diff = (o1.region.getOffset()+o1.region.getLength())-
(o2.region.getOffset()+o2.region.getLength());
if (diff == 0) {
return o1.objectId-o2.objectId;
}
return diff;
}
return 0;
}
};
public int compareTo(RegionSorter<T> o2) {
return RegionSorter.COMPARATOR.compare(this, o2);
}
}
/**
* A region node represents an object in a graph of regions in text. The graph can be
* used to discover overlapping regions. If a region node has an edge to another region
* node, then the two regions overlap.
* @author Del Myers
*
*/
public static class RegionNode<T> {
private IRegion region;
private T object;
private int objectId;
private List<RegionNode<T>> adjacencies;
/**
* Creates a new regionNode
* @param region
* @param object
* @param objectId
*/
protected RegionNode(IRegion region, T object, int objectId) {
this.region = region;
this.object = object;
this.objectId = objectId;
adjacencies = new LinkedList<RegionNode<T>>();
}
public List<RegionNode<T>> getAdjacencies() {
return adjacencies;
}
/**
* @return
*/
public T getObject() {
return object;
}
public IRegion getRegion() {
return region;
}
}
@SuppressWarnings("unchecked")
private static class NodeComparator implements Comparator<RegionNode> {
public int compare(RegionNode o1, RegionNode o2) {
int diff = o1.getAdjacencies().size() - o2.getAdjacencies().size();
if (diff == 0) {
return o1.objectId - o2.objectId;
}
return diff;
}
}
/**
* Represents a graph of overlapping regions. Ordered by the number of regions that one
* region overlaps with.
* @author Del Myers
*
*/
public static class RegionGraph<T> {
HashMap<T, RegionNode<T>> objectNodeMap;
private ArrayList<RegionNode<T>> N;
private T[] initialObjects;
public RegionGraph(IRegion[] regions, T[] objects) {
this.initialObjects = objects;
if (regions.length != objects.length) {
throw new IllegalArgumentException("regions must have matching objects");
}
TreeSet<RegionSorter<RegionNode<T>>> sorter = new TreeSet<RegionSorter<RegionNode<T>>>();
for (int i = 0; i < objects.length; i++) {
RegionNode<T> node = new RegionNode<T>(regions[i], objects[i], i);
sorter.add(new RegionSorter<RegionNode<T>>(regions[i], i, node, false));
sorter.add(new RegionSorter<RegionNode<T>>(regions[i], i, node, true));
}
objectNodeMap = new HashMap<T, RegionNode<T>>();
//build the graph
N = new ArrayList<RegionNode<T>>();
LinkedList<RegionNode<T>> heap = new LinkedList<RegionNode<T>>();
for (RegionSorter<RegionNode<T>> s : sorter) {
RegionNode<T> currentNode = s.object;
if (!s.isEnd) {
//connect to all other open nodes
for (RegionNode<T> n : heap) {
n.getAdjacencies().add(currentNode);
currentNode.getAdjacencies().add(n);
}
N.add(currentNode);
objectNodeMap.put(currentNode.object, currentNode);
heap.add(currentNode);
} else {
heap.remove(currentNode);
}
}
Collections.sort(N, new NodeComparator());
}
@SuppressWarnings("unchecked")
public T[] getOverlappingNodes(T object) {
RegionNode<T> node = objectNodeMap.get(object);
if (node != null) {
T[] result = (T[]) new Object[node.getAdjacencies().size()];
int i = 0;
for (RegionNode<T> adjacent : node.getAdjacencies()) {
result[i] = adjacent.object;
i++;
}
return result;
}
return null;
}
public int getOverlapCount(T object) {
RegionNode<T> node = objectNodeMap.get(object);
if (node != null) {
return node.adjacencies.size();
}
return -1;
}
public IRegion getRegion(T object) {
RegionNode<T> node = objectNodeMap.get(object);
if (node != null) {
return node.region;
}
return null;
}
public void remove(T object) {
RegionNode<T> node = objectNodeMap.get(object);
if (node != null) {
int index = Collections.binarySearch(N, node, new NodeComparator());
if (index > 0) {
RegionNode<T> found = N.get(index);
if (found == node) {
for (RegionNode<T> adjacent : node.getAdjacencies()) {
adjacent.getAdjacencies().remove(node);
}
objectNodeMap.remove(object);
N.remove(index);
Collections.sort(N, new NodeComparator());
}
}
}
}
public RegionNode<T> pop() {
if (N.size() > 0) {
RegionNode<T> top = N.remove(0);
for (RegionNode<T> adjacent : top.getAdjacencies()) {
adjacent.getAdjacencies().remove(top);
}
objectNodeMap.remove(top.object);
Collections.sort(N, new NodeComparator());
return top;
}
return null;
}
public T popObject() {
RegionNode<T> node = pop();
if (node != null) {
return node.getObject();
}
return null;
}
public RegionNode<T> peek() {
if (N.size() > 0) {
return N.get(0);
}
return null;
}
public T peekObject() {
RegionNode<T> node = peek();
if (node != null) {
return node.getObject();
}
return null;
}
@SuppressWarnings("unchecked")
public T[] getNodeObjects() {
T[] result = (T[])Array.newInstance(initialObjects.getClass().getComponentType(), N.size());
int i = 0;
for (RegionNode<T> node : N) {
result[i] = node.getObject();
i++;
}
return (T[])result;
}
}
/**
* Describes a matching between a waypoint and a waypoint descriptor. If both
* <code>waypoint</code> and <code>descriptor</code> have non-null values, then the
* two describe the same region in the text and the descriptor can safely be copied into
* the waypoint. If <code>waypoint</code> is non-null, and <code>descriptor</code> is
* null, then there is no matching waypoint for the descriptor. The converse is true
* as well.
*
* {@link WaypointParsingTools#organizeWaypoints(IFile, IWaypoint[], IParsedWaypointDescriptor[], String)}
* @author Del Myers
*
*/
public static class WaypointDescriptorMatch {
public final IWaypoint waypoint;
public final IParsedWaypointDescriptor descriptor;
private WaypointDescriptorMatch(IWaypoint waypoint, IParsedWaypointDescriptor descriptor) {
this.waypoint = waypoint;
this.descriptor = descriptor;
}
}
public static final boolean checkForOverlap(IParsedWaypointDescriptor[] descriptors) {
TreeSet<RegionSorter<IParsedWaypointDescriptor>> overLapChecker =
new TreeSet<RegionSorter<IParsedWaypointDescriptor>>();
for (int i = 0; i < descriptors.length; i++) {
int start = descriptors[i].getCharStart();
int length = descriptors[i].getCharEnd() - start;
Region r = new Region(start, length);
RegionSorter<IParsedWaypointDescriptor> ss = new RegionSorter<IParsedWaypointDescriptor>(r, i, descriptors[i], false);
RegionSorter<IParsedWaypointDescriptor> se = new RegionSorter<IParsedWaypointDescriptor>(r, i, descriptors[i], true);
overLapChecker.add(ss);
overLapChecker.add(se);
}
//pull off of the heap, ensuring that the start and end are well-formed
RegionSorter<IParsedWaypointDescriptor> last = null;
for (RegionSorter<IParsedWaypointDescriptor> s : overLapChecker) {
if (last == null) {
if (s.isEnd) return false;
else last = s;
} else {
//check against the last one.
if (!last.isEnd) {
//make sure that we have the right object.
if (!s.isEnd || last.objectId != s.objectId) {
return false;
}
//make sure that when we have the same object, the
//and end aren't the same.
if (s.region.getLength() == 0) return false;
} else {
if (s.isEnd || last.objectId == s.objectId) {
return false;
}
//make sure that when we have different objects,
//the start isn't the same.
if (s.region.getOffset()==last.region.getOffset()) return false;
}
}
}
return true;
}
/**
* Creates a graph of overlaps between parsed waypoint descriptors. Assumes that all descriptors are in
* the same file.
* @param allDescriptors
* @return
*/
public static final RegionGraph<IParsedWaypointDescriptor> calculateOverlap(List<? extends IParsedWaypointDescriptor> allDescriptors) {
IParsedWaypointDescriptor[] descriptorArray = new IParsedWaypointDescriptor[allDescriptors.size()];
IRegion[] regionArray = new IRegion[allDescriptors.size()];
for (int i = 0; i < allDescriptors.size(); i++) {
IParsedWaypointDescriptor desc = allDescriptors.get(i);
descriptorArray[i] = desc;
regionArray[i] = new Region(desc.getCharStart(), desc.getCharEnd()-desc.getCharStart());
}
return new RegionGraph<IParsedWaypointDescriptor>(regionArray, descriptorArray);
}
/**
* Creates a graph of overlaps between parsed waypoints. Assumes that all waypoints are in
* the same file.
* @param allDescriptors
* @return
*/
public static final RegionGraph<IWaypoint> calculateOverlappingWaypoints(List<? extends IWaypoint> allDescriptors) {
IWaypoint[] descriptorArray = new IWaypoint[allDescriptors.size()];
IRegion[] regionArray = new IRegion[allDescriptors.size()];
for (int i = 0; i < allDescriptors.size(); i++) {
IWaypoint desc = allDescriptors.get(i);
descriptorArray[i] = desc;
int start = ParsedWaypointUtils.getCharStart(desc);
int end = ParsedWaypointUtils.getCharEnd(desc);
regionArray[i] = new Region(start, end-start);
}
return new RegionGraph<IWaypoint>(regionArray, descriptorArray);
}
/**
* looks at the waypoints currently in the given file, and the waypoint descriptors provided
* to synchronize the new descriptors with the old waypoints. Matches by region only in
* order to improve efficiency.
*
* @param file
* @param oldWaypoints
* @param descriptors
* @param kind
*/
public static List<WaypointDescriptorMatch> organizeWaypoints(IFile file, IWaypoint[] oldWaypoints,
IParsedWaypointDescriptor[] descriptors, String kind) {
LinkedList<IParsedWaypointDescriptor> descriptorList = new LinkedList<IParsedWaypointDescriptor>();
descriptorList.addAll(Arrays.asList(descriptors));
LinkedList<WaypointDescriptorMatch> matches = new LinkedList<WaypointDescriptorMatch>();
for (int i = 0; i < oldWaypoints.length; i++) {
IWaypoint wp = oldWaypoints[i];
IFile wpFile =
ParsedWaypointPlugin.getDefault().getParsedWaypointRegistry().getFileForWaypoint(wp);
if (!file.equals(wpFile))
continue;
if (!wp.exists())
continue;
IParsedWaypointDescriptor match = null;
int wpStart = wp.getIntValue(IParsedWaypointAttributes.ATTR_CHAR_START, -1);
int wpEnd = wp.getIntValue(IParsedWaypointAttributes.ATTR_CHAR_END, -1);
if (wpStart == -1 || wpEnd == -1) continue;
for (Iterator<IParsedWaypointDescriptor> it = descriptorList.iterator(); it.hasNext();) {
IParsedWaypointDescriptor desc = it.next();
int descStart = desc.getCharStart();
int descEnd = desc.getCharEnd();
if (descStart == wpStart && descEnd == wpEnd) {
//match them.
match = desc;
it.remove();
break;
}
}
matches.add(new WaypointDescriptorMatch(wp, match));
}
//add all the unmatched descriptors
for (IParsedWaypointDescriptor desc : descriptorList) {
matches.add(new WaypointDescriptorMatch(null, desc));
}
return matches;
}
/**
* Looks at the list of matchings and either copies the matchings, deletes unmatched waypoints,
* or generates new waypoints based on the descriptors. Note: it is expected that this
* method is called within the context of an AbstractWaypointUpdateOperation.
* @param matchings
* @return a list of waypoints that were created/updated by this call. Excludes deleted waypoints.
*/
public static List<IWaypoint> generateWaypoints(List<WaypointDescriptorMatch> matchings, IFile file, String kind) {
List<IWaypoint> waypoints = new LinkedList<IWaypoint>();
for (WaypointDescriptorMatch match : matchings) {
if (match.waypoint != null && match.descriptor == null) {
deleteWaypoint(match.waypoint);
} else if (match.waypoint == null && match.descriptor != null) {
waypoints.add(createWaypoint(match.descriptor, file, kind));
} else if (match.waypoint != null && match.descriptor != null) {
copyDescriptor(match.waypoint, match.descriptor);
waypoints.add(match.waypoint);
}
}
return waypoints;
}
/**
* @param waypoint
* @param descriptor
*/
private static void copyDescriptor(final IWaypoint wp,
IParsedWaypointDescriptor descriptor) {
final ArrayList<String> attributes = new ArrayList<String>();
final ArrayList<Object> values = new ArrayList<Object>();
if (descriptor.getAuthor() != null) {
wp.setAuthor(descriptor.getAuthor());
attributes.add(IWaypoint.ATTR_AUTHOR);
values.add(descriptor.getAuthor());
}
if (descriptor.getDate() != null) {
wp.setDate(descriptor.getDate());
attributes.add(IWaypoint.ATTR_DATE);
DateFormat format = DateFormat.getDateInstance(DateFormat.SHORT, Locale.CANADA);
values.add(format.format(descriptor.getDate()));
}
if (descriptor.getMessage() != null) {
wp.setText(descriptor.getMessage());
attributes.add(IWaypoint.ATTR_MESSAGE);
values.add(descriptor.getMessage());
}
if (descriptor.getDetail() != null) {
wp.setStringValue(IParsedWaypointAttributes.ATTR_DOMAIN, descriptor.getDetail());
attributes.add(IParsedWaypointAttributes.ATTR_DOMAIN);
values.add(descriptor.getDetail());
}
wp.setIntValue(IParsedWaypointAttributes.ATTR_LINE, descriptor.getLine());
attributes.add(IParsedWaypointAttributes.ATTR_LINE);
values.add(descriptor.getLine());
//copy the tags
TreeSet<String> newTags = new TreeSet<String>();
TreeSet<ITag> oldTags = new TreeSet<ITag>();
for (ITag tag : wp.getTags()) {
oldTags.add(tag);
}
for (String tag : descriptor.getTags()) {
newTags.add(tag);
}
//remove orphaned tags
for (ITag tag : oldTags) {
if (!newTags.contains(tag.getName())) {
wp.removeTag(tag);
}
}
//add new ones
for (String tag : newTags) {
wp.addTag(tag);
}
String tagNames = "";
for (ITag tag : wp.getTags()) {
tagNames = tagNames + tag.getName() + " ";
}
tagNames.trim();
attributes.add("tags");
values.add(tagNames);
attributes.add(IParsedWaypointAttributes.ATTR_CHAR_START);
values.add(descriptor.getCharStart());
attributes.add(IParsedWaypointAttributes.ATTR_CHAR_START);
values.add(descriptor.getCharEnd());
attributes.add(IParsedWaypointAttributes.ATTR_KIND);
values.add(ParsedWaypointUtils.getKind(wp));
}
/**
* @param descriptor
* @param file
* @param kind
*/
private static IWaypoint createWaypoint(final IParsedWaypointDescriptor descriptor,
final IFile file, final String kind) {
final IWaypoint wp = TagSEAPlugin.getWaypointsModel().createWaypoint(ParsedWaypointPlugin.WAYPOINT_TYPE, descriptor.getTags());
if (wp != null) {
final String author = descriptor.getAuthor();
final Date date = descriptor.getDate();
final String message = descriptor.getMessage();
final String detail = descriptor.getDetail();
if (author != null) {
wp.setAuthor(author);
}
if (date != null) {
wp.setDate(date);
}
if (message != null) {
wp.setText(message);
}
if (detail != null) {
wp.setStringValue(IParsedWaypointAttributes.ATTR_DOMAIN, detail);
}
final int start = descriptor.getCharStart();
final int end = descriptor.getCharEnd();
wp.setIntValue(IParsedWaypointAttributes.ATTR_CHAR_START, start);
wp.setIntValue(IParsedWaypointAttributes.ATTR_CHAR_END, end);
wp.setStringValue(IParsedWaypointAttributes.ATTR_RESOURCE, file.getFullPath().toPortableString());
wp.setStringValue(IParsedWaypointAttributes.ATTR_KIND, kind);
wp.setIntValue(IParsedWaypointAttributes.ATTR_LINE, descriptor.getLine());
}
return wp;
}
/**
* @param waypoint
*/
public static void deleteWaypoint(final IWaypoint waypoint) {
if (waypoint.exists()) {
waypoint.delete();
}
}
}