package org.archstudio.xadl; import java.io.Serializable; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.archstudio.xarchadt.IXArchADTFeature; import org.archstudio.xarchadt.IXArchADTModelListener; import org.archstudio.xarchadt.IXArchADTQuery; import org.archstudio.xarchadt.IXArchADTTypeMetadata; import org.archstudio.xarchadt.ObjRef; import org.archstudio.xarchadt.XArchADTModelEvent; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; /** * Monitors an XPath for added, modified, and removed objRefs rooted at an arbitrary ObjRef. When a * change is detected, notifies listeners implementing {@link IXArchRelativePathTrackerListener}. * <p/> * There is limited support for filtering nodes by properties other than their name. You can filter: * <ul> * <li>by attribute value, e.g., "node[@attribute='value']",</li> * <li>by attribute contains substring, e.g., "node[contains(@attribute, 'substring')]", and</li> * <li>by name space, e.g., "node[*[namespace-uri()='name_space_URI']]".</li> * </ul> */ public final class XArchRelativePathTracker implements IXArchADTModelListener { /** * Captures a group in a regular expression that represents a name. E.g., "name". */ private static final String NAME_GROUP = "([a-zA-Z_][a-zA-Z0-9_]*)"; /** * Captures a group in a regular expression that represents a string. E.g., "'string'". */ private static final String STR_GROUP = "'([^']*)'"; /** * The pattern for defining an attribute constraint on an XPath segment. E.g., * "node[@attribute='value']". */ private static final Pattern ATTRIBUTE_PATTERN = Pattern.compile( "\\s*" + NAME_GROUP + "\\s*\\[\\s*@" + NAME_GROUP + "\\s*=\\s*" + STR_GROUP + "\\s*\\]"); /** * The pattern for defining a contains constraint on an XPath segment. E.g., * "node[contains(@attribute, 'value')]". */ private static final Pattern ATTRIBUTE_CONTAINS_PATTERN = Pattern.compile("\\s*" + NAME_GROUP + "\\s*\\[\\s*contains\\s*\\(@" + NAME_GROUP + "\\s*,\\s*" + STR_GROUP + "\\s*\\)\\s*\\]"); /** * The pattern for defining a name space constraint on an XPath segment. E.g., * "node[*[namespace-uri()='URL']]". */ private static final Pattern NAMESPACE_PATTERN = Pattern .compile("\\s*" + NAME_GROUP + "\\s*\\[\\s*\\*\\s*\\[\\s*namespace-uri\\s*\\(\\s*\\)\\s*=\\s*" + STR_GROUP + "\\s*\\]\\s*\\]"); /** * A predicate to check a node for a required attribute value. */ private static final class RequireAttributeValuePredicate implements Predicate<ObjRef> { IXArchADTQuery xarch; String attribute; String value; public RequireAttributeValuePredicate(IXArchADTQuery xarch, String attribute, String value) { this.xarch = xarch; this.attribute = attribute; this.value = value; } @Override public boolean apply(ObjRef input) { Object attrValue = xarch.get(input, attribute); if (attrValue != null) { return value.equals(attrValue.toString()); } return false; } } /** * A predicate to check a node for a required substring in an attribute. */ private static final class RequireAttributeContainsValuePredicate implements Predicate<ObjRef> { IXArchADTQuery xarch; String attribute; String value; public RequireAttributeContainsValuePredicate(IXArchADTQuery xarch, String attribute, String value) { this.xarch = xarch; this.attribute = attribute; this.value = value; } @Override public boolean apply(ObjRef input) { Object attrValue = xarch.get(input, attribute); if (attrValue != null) { return attrValue.toString().indexOf(value) >= 0; } return false; } } /** * A predicate to check a node for a required namespace. */ private static final class RequireNamespaceURIPredicate implements Predicate<ObjRef> { IXArchADTQuery xarch; String namespaceURI; public RequireNamespaceURIPredicate(IXArchADTQuery xarch, String namespaceURI) { this.xarch = xarch; this.namespaceURI = namespaceURI; } @Override public boolean apply(ObjRef input) { IXArchADTTypeMetadata typeMetadata = xarch.getTypeMetadata(input); return typeMetadata.getNsURI().equals(namespaceURI); } } /** * A node constraint in an XPath, consisting of a name and a predicate. */ private static final class Segment { final String name; final Predicate<ObjRef> predicate; public Segment(String name, Predicate<ObjRef> predicate) { this.name = name; this.predicate = predicate; } } /** * Creates a new list with the additional element appended to it. * * @param list The original list. * @param newElement The new element to add. * @return a new list with the additional element appended to it. */ private static final <T> List<T> append(List<T> list, T newElement) { List<T> newList = Lists.newArrayListWithCapacity(list.size() + 1); newList.addAll(list); newList.add(newElement); return newList; } /** * Creates a sublist of list, starting from the given index. Equivalent to * <code>list.subList(fromIndex, list.size())</code>. * * @param list The list from which to obtain the sublist. * @param fromIndex The beginning index of the sublist. * @return a sublist of list, starting from the given index. */ private static final <T> List<T> subList(List<T> list, int fromIndex) { if (list == null) { return null; } return list.subList(fromIndex, list.size()); } /** * Takes a path in the form of a string, selects the subpath from the given index, and returns the * subpath in the form of a string. * * @param path The starting path. * @param fromSegmentIndex The beginning segment index for the subpath. * @return a subpath starting at from the specified segment index. */ private static final String subPath(String path, int fromSegmentIndex) { if (path == null) { return null; } int index = 0; int segment = 0; while (segment++ < fromSegmentIndex) { index = path.indexOf('/', index) + 1; if (index <= 0) { if (segment == fromSegmentIndex) { index = path.length(); } else { throw new IllegalArgumentException( "Path \"" + path + "\" does not contain " + fromSegmentIndex + " segments."); } } } return path.substring(index); } /** * Parses a segment in an XPath (e.g., "node[@attribute='value']") into a node name and predicate * constraint. * * @param xarch The {@link IXArchADTQuery xarch} instance from which predicates should be * verified. * @param segment The segment text to parse. * @return a {@link Segment} containing the node name and predicate constraint. */ private static Segment parseSegment(IXArchADTQuery xarch, String segment) { Predicate<ObjRef> predicate = Predicates.alwaysTrue(); Matcher m; if ((m = ATTRIBUTE_PATTERN.matcher(segment)).find()) { segment = m.group(1); predicate = new RequireAttributeValuePredicate(xarch, m.group(2), m.group(3)); } if ((m = ATTRIBUTE_CONTAINS_PATTERN.matcher(segment)).find()) { segment = m.group(1); predicate = new RequireAttributeContainsValuePredicate(xarch, m.group(2), m.group(3)); } if ((m = NAMESPACE_PATTERN.matcher(segment)).find()) { segment = m.group(1); predicate = new RequireNamespaceURIPredicate(xarch, m.group(2)); } if (segment.indexOf('[') >= 0) { throw new IllegalArgumentException("Unrecognized XPath segment: " + segment); } return new Segment(segment, predicate); } private final IXArchADTQuery xarch; private final List<IXArchRelativePathTrackerListener> listeners = Lists.newCopyOnWriteArrayList(); /** The root ObjRef from which the XPath will be applied. */ private final ObjRef rootRef; /** The XPath in its original string format, e.g., "component/interface". */ private final String xPathStr; /** The XPath parsed into individual segments, e.g., "component" and "interface". */ private final List<Segment> xPath = Lists.newArrayList(); /** Whether the {@link #rootRef} is currently being monitored for changes. */ private boolean scanning = false; /** * The set of ObjRefs that have been added, with the list of ObjRefs connecting them to the * {@link #rootRef}. Keeping track of the ObjRefs from the rootRef is important because it allows * us to remove added ObjRefs if one of their ancestors connecting them to {@link #rootRef} is * removed. */ private final Map<ObjRef, List<ObjRef>> addedObjRefToLineageRefs = Maps.newHashMap(); /** * The set of all ObjRefs that match some segment of the XPath. This is used to detect when an * attribute change to an ObjRef effects whether that ObjRef matched the XPath segment. The value * is the segment index in {@link #xPath}. */ private final Map<ObjRef, Integer> positiveObjRefs = Maps.newHashMap(); /** * Creates a new XPath tracker. Note, this class must be added as a listener to the source of * change events for the document containing rootRef. * * @param xarch The {@link IXArchADTQuery xarch} instance on which to perform the XPath query. * @param rootRef The root ObjRef from which the XPath should be queried. * @param xPath The XPath to query. * @param startScanning If <code>true</code>, calls {@link #startScanning()} after initialization. */ public XArchRelativePathTracker(IXArchADTQuery xarch, ObjRef rootRef, String xPath, boolean startScanning) { this.xarch = Preconditions.checkNotNull(xarch); this.rootRef = Preconditions.checkNotNull(rootRef); this.xPathStr = Preconditions.checkNotNull(xPath); Preconditions.checkArgument(xPath.length() != 0, "Missing required XPath."); parseXPath(xPath); if (startScanning) { startScanning(); } } /** * Parses an XPath into individual {@link Segment}s. * * @param xPathStr The XPath string to parse. */ private void parseXPath(String xPathStr) { int startIndex = 0; int endIndex = 0; int bracesCount = 0; for (char ch : (xPathStr + '/').toCharArray()) { switch (ch) { case '[': bracesCount++; break; case ']': bracesCount--; break; case '/': if (bracesCount == 0) { xPath.add(parseSegment(xarch, xPathStr.substring(startIndex, endIndex))); startIndex = endIndex + 1; } break; } endIndex++; } Preconditions.checkArgument(bracesCount == 0, "Malformed XPath: %s", xPathStr); } /** * Adds the listener to the collection of listeners what will be informed of XPath query results. * * @param listener The listener to be added. */ public void addTrackerListener(IXArchRelativePathTrackerListener listener) { listeners.add(listener); } /** * Removes the listener from the collection of listeners what will be informed of XPath query * results. * * @param listener The listener to be removed. */ public void removeTrackerListener(IXArchRelativePathTrackerListener listener) { listeners.remove(listener); } /** * Fires an {@link IXArchRelativePathTrackerListener#processAdd(List, ObjRef) add event} to the * listeners of XPath query results. * * @param descendantRefs The descendant refs starting with the rootRef leading to addedRef, * inclusive. * @param addedRef The ObjRef that has been added. */ protected void fireProcessAdd(List<ObjRef> descendantRefs, ObjRef addedRef) { for (IXArchRelativePathTrackerListener l : listeners) { try { l.processAdd(descendantRefs, addedRef); } catch (Throwable t) { t.printStackTrace(); } } } /** * Fires an * {@link IXArchRelativePathTrackerListener#processUpdate(List, ObjRef, XArchADTModelEvent) update * event} to the listeners of XPath query results. * * @param descendantRefs The descendant refs starting with the rootRef leading to modifiedRef, * inclusive. * @param modifiedRef The ObjRef that was modified. * @param relativeEvt The relative event, rooted in the modified Ref. */ protected void fireProcessUpdate(List<ObjRef> descendantRefs, ObjRef modifiedRef, XArchADTModelEvent relEvt) { for (IXArchRelativePathTrackerListener l : listeners) { try { l.processUpdate(descendantRefs, modifiedRef, relEvt); } catch (Throwable t) { t.printStackTrace(); } } } /** * Fires a {@link IXArchRelativePathTrackerListener#processRemove(List, ObjRef) remove event} to * the listeners of XPath query results. * * @param descendantRefs The descendant refs starting with the rootRef leading to removedRef, * inclusive. * @param removedRef The ObjRef that has been removed. */ protected void fireProcessRemove(List<ObjRef> descendantRefs, ObjRef removedRef) { for (IXArchRelativePathTrackerListener l : listeners) { try { l.processRemove(descendantRefs, removedRef); } catch (Throwable t) { t.printStackTrace(); } } } /** * Returns the root ObjRef from which the XPath query is performed. * * @return the root ObjRef from which the XPath query is performed. */ public ObjRef getRootObjRef() { return rootRef; } /** * Returns the XPath query being tracked. * * @return the XPath query being tracked. */ public String getXPath() { return xPathStr; } /** * Returns the number of segments parsed from the xPath. * * @return the number of segments parsed from the xPath. */ public int getNumXPathSegments() { return xPath.size(); } /** * Starts monitoring, including scanning for existing ObjRefs and firing an * {@link IXArchRelativePathTrackerListener#processAdd(List, ObjRef) add event} for each. */ public void startScanning() { synchronized (addedObjRefToLineageRefs) { if (!scanning) { scanning = true; scanObjRef(Lists.newArrayList(rootRef)); } } } /** * Returns <code>true</code> if this tracker has been started, <code>false</code> otherwise. * * @return <code>true</code> if this tracker has been started, <code>false</code> otherwise. */ public boolean isScanning() { return scanning; } /** * Stops monitoring, including iterating through added ObjRefs and firing a * {@link IXArchRelativePathTrackerListener#processRemove(List, ObjRef) removal event} for each. */ public void stopScanning() { synchronized (addedObjRefToLineageRefs) { if (scanning) { scanning = false; for (List<ObjRef> objRefs : Lists.newArrayList(addedObjRefToLineageRefs.values())) { fireProcessRemove(objRefs, objRefs.get(objRefs.size() - 1)); } positiveObjRefs.clear(); addedObjRefToLineageRefs.clear(); } } } /** * Returns a copy of the ObjRefs currently resulting from the XPath query. * * @return a copy of the ObjRefs currently at the XPath. */ public Set<ObjRef> getAddedRefs() { return Sets.newHashSet(addedObjRefToLineageRefs.keySet()); } /** * Scans the last ObjRef of descendantRefs for ObjRefs matching the XPath. * * @param descendantRefs The descendants starting with {@link #rootRef} that match (a prefix of) * the XPath. */ protected void scanObjRef(List<ObjRef> descendantRefs) { // Ensure that the descendants starts with rootRef. Preconditions.checkArgument(descendantRefs.size() > 0); Preconditions.checkArgument(descendantRefs.get(0) == rootRef); // Ensure that the descendants do not exceed the XPath segment length. Preconditions.checkArgument(descendantRefs.size() <= xPath.size() + 1); // If the ObjRef matches the last segment of the XPath, add it. ObjRef objRef = descendantRefs.get(descendantRefs.size() - 1); positiveObjRefs.put(objRef, descendantRefs.size() - 2); if (descendantRefs.size() == xPath.size() + 1) { if (addedObjRefToLineageRefs.put(objRef, descendantRefs) == null) { fireProcessAdd(descendantRefs, objRef); } return; } // If not, check its children and check them against the corresponding XPath segment. Segment childSegment = xPath.get(descendantRefs.size() - 1); IXArchADTTypeMetadata type = xarch.getTypeMetadata(objRef); IXArchADTFeature feature = type.getFeatures().get(childSegment.name); if (feature == null) { throw new IllegalArgumentException(objRef + "(" + type.getTypeName() + ") doesn't contain feature " + childSegment.name + "."); } switch (feature.getType()) { case ELEMENT_SINGLE: { Object child = xarch.get(objRef, childSegment.name); if (child instanceof ObjRef) { if (childSegment.predicate.apply((ObjRef) child)) { scanObjRef(append(descendantRefs, (ObjRef) child)); } } else if (child != null) { throw new IllegalArgumentException("Feature " + childSegment.name + " of " + objRef + "(" + type.getTypeName() + ") doesn't contain an ObjRef for XPath " + xPathStr + "."); } } break; case ELEMENT_MULTIPLE: { for (Serializable child : xarch.getAll(objRef, childSegment.name)) { if (child instanceof ObjRef) { if (childSegment.predicate.apply((ObjRef) child)) { scanObjRef(append(descendantRefs, (ObjRef) child)); } } else if (child != null) { throw new IllegalArgumentException( "Feature " + childSegment.name + " of " + objRef + "(" + type.getTypeName() + ") doesn't contain an ObjRef for XPath " + xPathStr + "."); } } } break; default: throw new IllegalArgumentException("Feature " + childSegment.name + " of " + objRef + "(" + type.getTypeName() + ") is an invalid type: " + feature.getType().name() + "."); } } /** * Removes added ObjRefs that have been a part of a positive match of the XPath. This is * accomplished by scanning each value in {@link #addedObjRefToLineageRefs} and removing the * corresponding keys. * * @param objRef The ObjRef in the lineage of ObjRefs that should be removed. * @return <code>true</code> if ObjRefs were removed, <code>false</code> otherwise. */ protected boolean removeObjRef(ObjRef objRef) { boolean removedRefs = false; Integer xPathIndexInt = positiveObjRefs.remove(objRef); if (xPathIndexInt != null) { int xPathIndex = xPathIndexInt; // Gather the lineage refs of the ObjRefs that are to be removed. List<List<ObjRef>> removedLineageRefs = Lists.newArrayListWithCapacity(addedObjRefToLineageRefs.size()); for (Iterator<Entry<ObjRef, List<ObjRef>>> i = addedObjRefToLineageRefs.entrySet().iterator(); i.hasNext();) { List<ObjRef> lineageRefs = i.next().getValue(); if (lineageRefs.get(xPathIndex + 1).equals(objRef)) { i.remove(); removedLineageRefs.add(lineageRefs); } } // Remove them. removedRefs = removedLineageRefs.size() > 0; for (List<ObjRef> lineageRefs : removedLineageRefs) { // Remove the ObjRefs from the set of positive ObjRefs. for (int i = xPathIndex + 1; i < lineageRefs.size(); ++i) { positiveObjRefs.remove(lineageRefs.get(i)); } // Fire the removal event. fireProcessRemove(lineageRefs, lineageRefs.get(lineageRefs.size() - 1)); } } return removedRefs; } /** * Returns whether the descendants match the XPath. * * @param descendantRefs The descendants, starting with {@link #rootRef}, to test agains the * XPath. May be <code>null</code> if only names should be checked. * @param descendantNames The names of the descendants, <i>excluding</i> that for the rootRef, to * test against the XPath. * @return <code>true</code> if the descendants match, <code>false</code> otherwise. */ protected boolean xPathMatches(List<ObjRef> descendantRefs, List<String> descendantNames) { if (descendantRefs != null) { // Ensure that the descendants starts with rootRef. Preconditions.checkArgument(descendantRefs.size() > 0); Preconditions.checkArgument(descendantRefs.get(0) == rootRef); // Ensure that the descendants are of the same size. Preconditions.checkArgument(descendantRefs.size() == descendantNames.size() + 1); } // Check that the names match. for (int i = 0; i < xPath.size() && i < descendantNames.size(); ++i) { if (!xPath.get(i).name.equals(descendantNames.get(i))) { return false; } } if (descendantRefs != null) { // Check that the preconditions match. for (int i = 0; i < xPath.size() && i + 1 < descendantRefs.size(); ++i) { if (!xPath.get(i).predicate.apply(descendantRefs.get(i + 1))) { return false; } } } return true; } @Override public void handleXArchADTModelEvent(XArchADTModelEvent evt) { if (scanning) { synchronized (addedObjRefToLineageRefs) { // Indicates that an update event is unnecessary, e.g., because an add or remove event // already occurred. boolean skipUpdateEvent = false; // If an ObjRef was removed (or replaced) create a remove event. switch (evt.getEventType()) { case CLEAR: // Fall through. case REMOVE: // Fall through. case SET: if (evt.getOldValue() instanceof ObjRef) { ObjRef oldRef = (ObjRef) evt.getOldValue(); // Check whether the removed ObjRef is a candidate for an XPath match. List<ObjRef> descendants = Lists.reverse(evt.getOldValueAncestors()); int indexOfRootRef = descendants.indexOf(rootRef); if (indexOfRootRef < 0) { return; } descendants = subList(descendants, indexOfRootRef); // Determine whether oldRef causes the removal of an ObjRef. if (descendants.size() > xPath.size() + 1) { break; } // If all descendants are in positiveObjRefs then we remove it. for (ObjRef descendantRef : descendants) { if (!positiveObjRefs.containsKey(descendantRef)) { return; } } if (removeObjRef(oldRef)) { skipUpdateEvent = true; break; } } break; default: // Do nothing. break; } // If an ObjRef was added (or replaced) create an add event. switch (evt.getEventType()) { case ADD: // Fall through. case SET: if (evt.getNewValue() instanceof ObjRef) { // We don't process the addition of the rootRef itself. ObjRef newRef = (ObjRef) evt.getNewValue(); if (rootRef.equals(newRef)) { break; } // Determine if newRef is related to rootRef. List<ObjRef> descendants = Lists.reverse(evt.getNewValueAncestors()); int indexOfRootRef = descendants.indexOf(rootRef); if (indexOfRootRef < 0) { return; } descendants = subList(descendants, indexOfRootRef); // Determine whether newRef causes the addition of an ObjRef. if (descendants.size() > xPath.size() + 1) { break; } List<String> path = Arrays.asList(evt.getNewValuePath().split("/")); path = subList(path, indexOfRootRef); if (!xPathMatches(descendants, path)) { break; } // Scan for new ObjRefs. scanObjRef(descendants); skipUpdateEvent = true; } break; default: // Do nothing. break; } // Changing an attribute can cause an ObjRef to match or not match an XPath segment. switch (evt.getEventType()) { case CLEAR: // Fall through. case SET: // We already handled ObjRef changes above. if (evt.getOldValue() instanceof ObjRef) { break; } if (evt.getNewValue() instanceof ObjRef) { break; } // Check whether the newly modified ObjRef is a candidate for an XPath match. List<ObjRef> descendants = Lists.reverse(evt.getSourceAncestors()); int indexOfRootRef = descendants.indexOf(rootRef); if (indexOfRootRef < 0) { return; } descendants = subList(descendants, indexOfRootRef); if (descendants.size() > xPath.size() + 1) { break; } // Check whether the ObjRef matched before and if it is the same now. List<String> path = Arrays.asList(evt.getSourcePath().split("/")); path = subList(path, indexOfRootRef); if (xPathMatches(descendants, path)) { if (!positiveObjRefs.containsKey(evt.getSource())) { scanObjRef(descendants); skipUpdateEvent = true; } } else { if (removeObjRef(evt.getSource())) { skipUpdateEvent = true; } } break; default: // Do nothing. break; } // If an addition/removal event already occurred, skip sending an update event. if (skipUpdateEvent) { return; } // Get the relative descendants under rootRef. List<ObjRef> descendants = Lists.reverse(evt.getSourceAncestors()); int indexOfRootRef = descendants.indexOf(rootRef); if (indexOfRootRef < 0) { return; } descendants = subList(descendants, indexOfRootRef); // Send an event if the update is under an added ref. if (descendants.size() > xPath.size()) { ObjRef addedRef = descendants.get(xPath.size()); if (addedObjRefToLineageRefs.containsKey(addedRef)) { // Make an event relative to the added ref. int addedRefIndex = indexOfRootRef + xPath.size(); XArchADTModelEvent relEvt = new XArchADTModelEvent(evt.getEventType(), evt.getSource(), evt.getSourceAncestors().subList(0, evt.getSourceAncestors().size() - addedRefIndex), subPath(evt.getSourcePath(), addedRefIndex), evt.getFeatureName(), evt.getOldValue(), subPath(evt.getOldValuePath(), addedRefIndex), evt.getNewValue(), subPath(evt.getNewValuePath(), addedRefIndex)); fireProcessUpdate(descendants, addedRef, relEvt); } } } } } @Override public String toString() { return "rootRef=" + rootRef + "(" + xarch.getTypeMetadata(rootRef).getTypeName() + "), " + // "xPath=" + xPathStr + ", " + "scanning=" + scanning; } }