/******************************************************************************* * Copyright 2005-2006, CHISEL Group, University of Victoria, Victoria, BC, Canada. * 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.c.waypoints; import java.text.DateFormat; import java.util.Arrays; import java.util.Date; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TreeMap; import net.sourceforge.tagsea.c.ICWaypointAttributes; import net.sourceforge.tagsea.c.waypoints.parser.CWaypointInfo; import net.sourceforge.tagsea.c.waypoints.parser.IParsedCWaypointInfo; import net.sourceforge.tagsea.c.waypoints.parser.WaypointParseProblem; import net.sourceforge.tagsea.core.IWaypoint; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.Region; /** * Java waypoint information that can be changed dynamically in order to create text changes * on a document. * @author Del Myers */ public class MutableCWaypointInfo extends CWaypointInfo implements IParsedCWaypointInfo { public static enum TagStyle { /** * new tags will be added using dot syntax. */ DOT, /** * new tags will be added using parenthesis syntax */ PARENETHESIS, } /** * A class that represents a change to the internal structure of a MutableJavaWaypointInfo. * All offsets are relative to the internal text data of this info, not to the external document * in which the waypoint can be found. In order to translate changes to the internal structure * to changes in the external structure, add the waypoint info's offest (found by calling * getOffset() on IJavaWaypointInfo) to the text replacement's offset. * @author Del Myers */ public static class TextReplacement { /** * The offset that the replacement takes place at. */ public final int offset; /** * The length of text replaced. */ public final int length; /** * The inserted text. */ public final String text; TextReplacement(int offset, int length, String text) { this.offset = offset; this.length = length; this.text = text; } } TreeMap<String, String> attributes; TreeMap<String, IRegion> attributeRegionMap; TreeMap<String, List<IRegion>> tagRegionMap; private LinkedList<TextReplacement> replacementStack; private int offset; private String text; private TagStyle preferredStyle; private boolean deleted; /** * Creates a copy of the given prototype. * @param prototype */ public MutableCWaypointInfo(IParsedCWaypointInfo prototype, TagStyle preferredStyle) { if (prototype.getProblems().length > 0) { throw new UnsupportedOperationException(Messages.getString("MutableJavaWaypointInfo.problemError")); //$NON-NLS-1$ } this.replacementStack = new LinkedList<TextReplacement>(); this.preferredStyle = preferredStyle; Map<String, String> attributes = prototype.getAttributes(); this.attributes = new TreeMap<String, String>(); this.attributeRegionMap = new TreeMap<String, IRegion>(); for (String key : attributes.keySet()) { this.attributes.put(key, attributes.get(key)); this.attributeRegionMap.put(key, prototype.getRegionForAttribute(key)); } this.tagRegionMap = new TreeMap<String, List<IRegion>>(); for (String tag : prototype.getTags()) { List<IRegion> regions = new LinkedList<IRegion>(); regions.addAll(Arrays.asList(prototype.getRegionsForTag(tag))); tagRegionMap.put(tag, regions); } this.offset = prototype.getOffset(); this.text = prototype.getWaypointData(); //adjust the text so that all the new-linish characters are chomped //from the end. if (this.text.length() - 1 >= 0) { char c = this.text.charAt(this.text.length()-1); int end = this.text.length(); while (c == '\n' || c == '\r') { end--; } this.text = this.text.substring(0, end); } this.deleted = false; } public MutableCWaypointInfo() { this.preferredStyle = TagStyle.PARENETHESIS; this.replacementStack = new LinkedList<TextReplacement>(); this.attributeRegionMap = new TreeMap<String, IRegion>(); this.tagRegionMap = new TreeMap<String, List<IRegion>>(); this.offset = 0; this.text = "@tag "; //$NON-NLS-1$ this.deleted = false; } /* (non-Javadoc) * @see net.sourceforge.tagsea.java.waypoints.parser.IParsedJavaWaypointInfo#getOffset() */ public int getOffset() { return offset; } /* (non-Javadoc) * @see net.sourceforge.tagsea.java.waypoints.parser.IParsedJavaWaypointInfo#getProblems() */ public WaypointParseProblem[] getProblems() { return new WaypointParseProblem[0]; } /* (non-Javadoc) * @see net.sourceforge.tagsea.java.waypoints.parser.IParsedJavaWaypointInfo#getRegionForAttribute(java.lang.String) */ public IRegion getRegionForAttribute(String key) { return attributeRegionMap.get(key); } /* (non-Javadoc) * @see net.sourceforge.tagsea.java.waypoints.parser.IParsedJavaWaypointInfo#getRegionForDescription() */ public IRegion getRegionForDescription() { int end = 0; for (List<IRegion> regions : tagRegionMap.values()) { for (IRegion region : regions) { end = Math.max(region.getOffset() + region.getLength(), end); } } for (IRegion region : attributeRegionMap.values()) { end = Math.max(region.getOffset() + region.getLength(), end); } for (; end < text.length() && text.charAt(end) != ':'; end++) {} if (end >= text.length()) return null; return new Region(end, text.length()-end); } /* (non-Javadoc) * @see net.sourceforge.tagsea.java.waypoints.parser.IParsedJavaWaypointInfo#getRegionsForTag(java.lang.String) */ public IRegion[] getRegionsForTag(String tag) { List<IRegion> regions = tagRegionMap.get(tag); if (regions != null) { return (IRegion[])regions.toArray(new IRegion[regions.size()]); } return new IRegion[0]; } /* (non-Javadoc) * @see net.sourceforge.tagsea.java.waypoints.parser.IParsedJavaWaypointInfo#getWaypointData() */ public String getWaypointData() { return text; } /* (non-Javadoc) * @see net.sourceforge.tagsea.java.waypoints.parser.IJavaWaypointInfo#getAttributes() */ public Map<String, String> getAttributes() { throw new UnsupportedOperationException(); } /* (non-Javadoc) * @see net.sourceforge.tagsea.java.waypoints.parser.IJavaWaypointInfo#getDescription() */ public String getDescription() { IRegion region = getRegionForDescription(); if (region.getLength() > 0) { return text.substring(region.getOffset()); } return ""; //$NON-NLS-1$ } /* (non-Javadoc) * @see net.sourceforge.tagsea.java.waypoints.parser.IJavaWaypointInfo#getLength() */ public int getLength() { return text.length(); } /* (non-Javadoc) * @see net.sourceforge.tagsea.java.waypoints.parser.IJavaWaypointInfo#getTags() */ public String[] getTags() { return tagRegionMap.keySet().toArray(new String[0]); } /** * Deletes the entire text of the waypoint. All information will be deleted and no more * incoming changes will be processed. * */ public void delete() { attributeRegionMap.clear(); tagRegionMap.clear(); TextReplacement replacement = new TextReplacement(0, text.length(), ""); //$NON-NLS-1$ applyReplacement(replacement); this.deleted = true; } public boolean isDeleted() { return this.deleted; } /** * Adds the given tag to the waypoint information using the preferred tag style. */ public void addTag(String name) { if (isDeleted()) return; //do nothing if the tag already exists. if (tagRegionMap.containsKey(name)) return; switch (preferredStyle) { case DOT: addTagAsDot(name, findEndOfTags()); break; case PARENETHESIS: addTagAsParenthesis(name, findEndOfTags()); break; } } /** * Finds the index after the last character for the largest tag region. * @return */ private int findEndOfTags() { int end = 0; for (List<IRegion> regions : tagRegionMap.values()) { for (IRegion region : regions) { end = Math.max(region.getOffset() + region.getLength(), end); } } //find the beginning of the attributes. if (end == 0) { for (IRegion region : attributeRegionMap.values()) { end = Math.min(region.getOffset()-1, end); } } if (end == 0) { IRegion region = getRegionForDescription(); if (region == null) { end = this.text.length(); } else { //the end will be at the ':' we want to add before the colon. end = region.getOffset()-1; } } if (end == 0) { end = text.length(); } return end; } private int findEndOfAttributes() { int end = 0; for (IRegion region : attributeRegionMap.values()) { end = Math.max(region.getOffset() + region.getLength(), end); } if (end == 0) { for (List<IRegion> regions : tagRegionMap.values()) { for (IRegion region : regions) { end = Math.max(region.getOffset() + region.getLength(), end); } } } if (end == 0) { IRegion region = getRegionForDescription(); if (region == null) { end = this.text.length(); } else { end = region.getOffset()-2; } } return end; } /** * @param name */ private void addTagAsParenthesis(String name, int offset) { String dotName = name; name = getParenthesisTag(name); List<IRegion> regions = tagRegionMap.get(name); if (regions == null) { regions = new LinkedList<IRegion>(); tagRegionMap.put(dotName, regions); } String prefix = ""; //$NON-NLS-1$ if (offset == text.length() || !Character.isWhitespace(text.charAt(offset-1))) { prefix = " "; //$NON-NLS-1$ } String suffix = ""; //$NON-NLS-1$ if (offset < text.length()-1 && (!Character.isWhitespace(text.charAt(offset)))) { suffix = " "; //$NON-NLS-1$ } applyReplacement(new TextReplacement(offset, 0, prefix+name+suffix)); regions.add(new Region(offset+prefix.length(), name.length())); } private String getParenthesisTag(String tagName) { int dotCount = 0; for (int i = 0; i < tagName.length(); i++) { if (tagName.charAt(i) == '.') { dotCount++; } } String name = tagName.replace('.', '('); for (int i = 0; i < dotCount; i++) { name+=')'; } return name; } /** * @param name */ private void addTagAsDot(String name, int offset) { List<IRegion> regions = tagRegionMap.get(name); if (regions == null) { regions = new LinkedList<IRegion>(); tagRegionMap.put(name, regions); } String prefix = ""; //$NON-NLS-1$ if (offset == text.length() || !Character.isWhitespace(text.charAt(offset-1))) { prefix = " "; //$NON-NLS-1$ } String suffix = ""; //$NON-NLS-1$ if (offset < text.length()-1 && (!Character.isWhitespace(text.charAt(offset)))) { suffix = " "; //$NON-NLS-1$ } applyReplacement(new TextReplacement(offset, 0, prefix + name + suffix)); regions.add(new Region(offset+prefix.length(), name.length())); } public void changeTag(String oldName, String newName) { if (!tagRegionMap.containsKey(oldName)) return; List<IRegion> newRegions = tagRegionMap.get(newName); if (newRegions == null) { newRegions = new LinkedList<IRegion>(); tagRegionMap.put(newName, newRegions); } //use the removal logic, but we want to make sure that //all the tags are replaced back at the same offset. List<IRegion> oldRegions = tagRegionMap.get(oldName); while (oldRegions.size() > 0) { IRegion region = oldRegions.remove(0); String oldText = text.substring(region.getOffset(), region.getOffset()+region.getLength()); String name = newName; if (oldText.indexOf('(') != -1) { name = getParenthesisTag(newName); } applyReplacement(new TextReplacement(region.getOffset(), region.getLength(), name)); newRegions.add(new Region(region.getOffset(), name.length())); } tagRegionMap.remove(oldName); } private void applyReplacement(TextReplacement replacement) { if (replacement.offset == text.length() + 1) { if (replacement.length != 0) { throw new IndexOutOfBoundsException(); } this.text += replacement.text; } else if (replacement.offset > text.length()) { throw new IndexOutOfBoundsException(); } else { this.text = text.substring(0, replacement.offset)+ replacement.text + text.substring(replacement.offset+replacement.length, text.length()); // adjust all the regions that come after the replacement. for (List<IRegion> regions : tagRegionMap.values()) { List<IRegion> regionsToRemove = new LinkedList<IRegion>(); List<IRegion> fixedRegions = new LinkedList<IRegion>(); for (IRegion region : regions) { if (region.getOffset() > replacement.offset) { regionsToRemove.add(region); fixedRegions.add(new Region(region.getOffset()+(replacement.text.length()-replacement.length), region.getLength())); } } regions.removeAll(regionsToRemove); regions.addAll(fixedRegions); } for (String attr : attributeRegionMap.keySet()) { IRegion region = attributeRegionMap.get(attr); if (region.getOffset() > replacement.offset) { IRegion r = new Region(region.getOffset()+(replacement.text.length()-replacement.length), region.getLength()); attributeRegionMap.put(attr, r); } } } replacementStack.add(replacement); } /** * Sets the given attribute to the given value. The value cannot contain any double * quotation marks (") or end-of-line characters (\n \r). If any of these exist, an * IllegalArgumentException will be thrown. * * "length", "offset", "file", and "javaElement" are illegal names and will cause an * IllegalArgumentException to be thrown. * * @param name * @param value */ public void setAttribute(String name, String value) throws IllegalArgumentException { if (ICWaypointAttributes.ATTR_RESOURCE.equals(name) || ICWaypointAttributes.ATTR_CHAR_END.equals(name) || ICWaypointAttributes.ATTR_CHAR_START.equals(name) ) { throw new IllegalArgumentException(Messages.getString("MutableJavaWaypointInfo.illegalAttribute") + name); //$NON-NLS-1$ } if (isDeleted()) return; if (IWaypoint.ATTR_MESSAGE.equals(name)) { setDescription(value); return; } IRegion region = getRegionForAttribute(name); if (value == null || "".equals(value)) { //$NON-NLS-1$ //simply remove the old one. if (region != null) { attributeRegionMap.remove(name); applyReplacement(new TextReplacement(region.getOffset()-1, region.getLength()+1, "")); //$NON-NLS-1$ return; } } boolean needsQuotes = false; for (int i = 0 ; i < value.length(); i++) { char c = value.charAt(i); if (c == '\r' || c == '\n' || c == '"') { throw new IllegalArgumentException(Messages.getString("MutableJavaWaypointInfo.illegalCharacter") + c); //$NON-NLS-1$ } if (!needsQuotes && (Character.isWhitespace(c) || c == ':')) { needsQuotes = true; } } if (needsQuotes) { value = '"' + value + '"'; } String insertString = "-"+name+"="+value; //$NON-NLS-1$ //$NON-NLS-2$ if (region == null) { //just add the attribute to the end. int offset = findEndOfAttributes(); String prefix = ""; //$NON-NLS-1$ if (offset == text.length() || !Character.isWhitespace(text.charAt(offset-1))) { prefix = " "; //$NON-NLS-1$ } String suffix = ""; //$NON-NLS-1$ if (offset < text.length()-1 && (!Character.isWhitespace(text.charAt(offset)))) { suffix = " "; //$NON-NLS-1$ } applyReplacement(new TextReplacement(offset, 0, prefix+insertString+suffix)); attributeRegionMap.put(name, new Region(offset+prefix.length(), insertString.length())); } else { applyReplacement(new TextReplacement(region.getOffset(), region.getLength(), insertString)); attributeRegionMap.put(name, new Region(region.getOffset(), insertString.length())); } } public void setDescription(String description) { if (isDeleted()) return; IRegion region = getRegionForDescription(); if (region == null ) { int offset = text.length(); String insertString = " :" + description; //$NON-NLS-1$ applyReplacement(new TextReplacement(offset, 0, insertString)); } else { applyReplacement(new TextReplacement(region.getOffset()+1, region.getLength()-1, description)); } } public void setAuthor(String author) { if (isDeleted()) return; setAttribute(IWaypoint.ATTR_AUTHOR, author); } /** * Sets the date using the default locale. * @param date */ public void setDate(Date date) { if (isDeleted()) return; if (date == null) { setAttribute(IWaypoint.ATTR_DATE, null); return; } Locale loc = Locale.getDefault(); String dateString = DateFormat.getDateInstance(DateFormat.SHORT, loc).format(date); dateString = loc.getLanguage()+loc.getCountry().toUpperCase()+':'+dateString; setAttribute(IWaypoint.ATTR_DATE, dateString); } public Iterator<TextReplacement> getReplacementIterator() { return replacementStack.iterator(); } public void removeTag(String name) { if (isDeleted()) return; List<IRegion> regions = tagRegionMap.get(name); if (regions == null) return; while (regions.size() > 0) { IRegion region = regions.remove(0); //there will be whitespace before the tag. TextReplacement replacement = new TextReplacement(region.getOffset()-1, region.getLength()+1, ""); //$NON-NLS-1$ applyReplacement(replacement); } } /** * Sets the offset in the text at which the waypoint occurs. * @param offset */ public void setOffsetInText(int offset) { this.offset = offset; } }