/******************************************************************************* * Copyright (c) 2000, 2016 IBM Corporation and others. * 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: * IBM Corporation - initial API and implementation * Vladimir Piskarev <pisv@1c.ru> - Building large Java element deltas is really slow - https://bugs.eclipse.org/443928 *******************************************************************************/ package org.eclipse.jdt.internal.core; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import org.eclipse.core.resources.IResourceDelta; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IJavaElementDelta; import org.eclipse.jdt.core.dom.CompilationUnit; /** * @see IJavaElementDelta */ @SuppressWarnings({"rawtypes", "unchecked"}) public class JavaElementDelta extends SimpleDelta implements IJavaElementDelta { /** * @see #getAffectedChildren() */ IJavaElementDelta[] affectedChildren = EMPTY_DELTA; /* * The AST created during the last reconcile operation. * Non-null only iff: * - in a POST_RECONCILE event * - an AST was requested during the last reconcile operation * - the changed element is an ICompilationUnit in working copy mode */ CompilationUnit ast = null; /* * The element that this delta describes the change to. */ IJavaElement changedElement; /** * Collection of resource deltas that correspond to non java resources deltas. */ IResourceDelta[] resourceDeltas = null; /** * Counter of resource deltas */ int resourceDeltasCounter; /** * @see #getMovedFromElement() */ IJavaElement movedFromHandle = null; /** * @see #getMovedToElement() */ IJavaElement movedToHandle = null; IJavaElementDelta[] annotationDeltas = EMPTY_DELTA; /** * Empty array of IJavaElementDelta */ static IJavaElementDelta[] EMPTY_DELTA= new IJavaElementDelta[] {}; /** * Child index is needed iff affectedChildren.length >= NEED_CHILD_INDEX */ static int NEED_CHILD_INDEX = 3; /** * On-demand index into affectedChildren */ Map<Key, Integer> childIndex; public boolean ignoreFromTests = false; /** * The delta key */ protected static class Key { public final IJavaElement element; public Key(IJavaElement element) { this.element = element; } public int hashCode() { return this.element.hashCode(); } public boolean equals(Object obj) { if (!(obj instanceof Key)) return false; return equalsAndSameParent(this.element, ((Key) obj).element); } } /** * Creates the root delta. To create the nested delta * hierarchies use the following convenience methods. The root * delta can be created at any level (for example: project, package root, * package fragment...). * <ul> * <li><code>added(IJavaElement)</code> * <li><code>changed(IJavaElement)</code> * <li><code>moved(IJavaElement, IJavaElement)</code> * <li><code>removed(IJavaElement)</code> * <li><code>renamed(IJavaElement, IJavaElement)</code> * </ul> */ public JavaElementDelta(IJavaElement element) { this.changedElement = element; } /** * Adds the child delta to the collection of affected children. If the * child is already in the collection, walk down the hierarchy. */ protected void addAffectedChild(JavaElementDelta child) { switch (this.kind) { case ADDED: case REMOVED: // no need to add a child if this parent is added or removed return; case CHANGED: this.changeFlags |= F_CHILDREN; break; default: this.kind = CHANGED; this.changeFlags |= F_CHILDREN; } // if a child delta is added to a compilation unit delta or below, // it's a fine grained delta if (this.changedElement.getElementType() >= IJavaElement.COMPILATION_UNIT) { fineGrained(); } Key childKey = new Key(child.getElement()); Integer existingChildIndex = getChildIndex(childKey); if (existingChildIndex == null) { //new affected child addNewChild(child); } else { JavaElementDelta existingChild = (JavaElementDelta) this.affectedChildren[existingChildIndex]; switch (existingChild.getKind()) { case ADDED: switch (child.getKind()) { case ADDED: // child was added then added -> it is added case CHANGED: // child was added then changed -> it is added return; case REMOVED: // child was added then removed -> noop removeExistingChild(childKey, existingChildIndex); return; } break; case REMOVED: switch (child.getKind()) { case ADDED: // child was removed then added -> it is changed child.kind = CHANGED; this.affectedChildren[existingChildIndex] = child; return; case CHANGED: // child was removed then changed -> it is removed case REMOVED: // child was removed then removed -> it is removed return; } break; case CHANGED: switch (child.getKind()) { case ADDED: // child was changed then added -> it is added case REMOVED: // child was changed then removed -> it is removed this.affectedChildren[existingChildIndex] = child; return; case CHANGED: // child was changed then changed -> it is changed IJavaElementDelta[] children = child.getAffectedChildren(); for (int i = 0; i < children.length; i++) { JavaElementDelta childsChild = (JavaElementDelta) children[i]; existingChild.addAffectedChild(childsChild); } // update flags boolean childHadContentFlag = (child.changeFlags & F_CONTENT) != 0; boolean existingChildHadChildrenFlag = (existingChild.changeFlags & F_CHILDREN) != 0; existingChild.changeFlags |= child.changeFlags; // remove F_CONTENT flag if existing child had F_CHILDREN flag set // (case of fine grained delta (existing child) and delta coming from // DeltaProcessor (child)) if (childHadContentFlag && existingChildHadChildrenFlag) { existingChild.changeFlags &= ~F_CONTENT; } // add the non-java resource deltas if needed // note that the child delta always takes precedence over this existing child delta // as non-java resource deltas are always created last (by the DeltaProcessor) IResourceDelta[] resDeltas = child.getResourceDeltas(); if (resDeltas != null) { existingChild.resourceDeltas = resDeltas; existingChild.resourceDeltasCounter = child.resourceDeltasCounter; } return; } break; default: // unknown -> existing child becomes the child with the existing child's flags int flags = existingChild.getFlags(); this.affectedChildren[existingChildIndex] = child; child.changeFlags |= flags; } } } /** * Creates the nested deltas resulting from an add operation. * Convenience method for creating add deltas. * The constructor should be used to create the root delta * and then an add operation should call this method. */ public void added(IJavaElement element) { added(element, 0); } public void added(IJavaElement element, int flags) { JavaElementDelta addedDelta = new JavaElementDelta(element); addedDelta.added(); addedDelta.changeFlags |= flags; insertDeltaTree(element, addedDelta); } /** * Adds the new child delta to the collection of affected children. */ protected void addNewChild(JavaElementDelta child) { this.affectedChildren = growAndAddToArray(this.affectedChildren, child); if (this.childIndex != null) { this.childIndex.put(new Key(child.getElement()), this.affectedChildren.length - 1); } } /** * Adds the child delta to the collection of affected children. If the * child is already in the collection, walk down the hierarchy. */ protected void addResourceDelta(IResourceDelta child) { switch (this.kind) { case ADDED: case REMOVED: // no need to add a child if this parent is added or removed return; case CHANGED: this.changeFlags |= F_CONTENT; break; default: this.kind = CHANGED; this.changeFlags |= F_CONTENT; } if (this.resourceDeltas == null) { this.resourceDeltas = new IResourceDelta[5]; this.resourceDeltas[this.resourceDeltasCounter++] = child; return; } if (this.resourceDeltas.length == this.resourceDeltasCounter) { // need a resize System.arraycopy(this.resourceDeltas, 0, (this.resourceDeltas = new IResourceDelta[this.resourceDeltasCounter * 2]), 0, this.resourceDeltasCounter); } this.resourceDeltas[this.resourceDeltasCounter++] = child; } /** * Creates the nested deltas resulting from a change operation. * Convenience method for creating change deltas. * The constructor should be used to create the root delta * and then a change operation should call this method. */ public JavaElementDelta changed(IJavaElement element, int changeFlag) { JavaElementDelta changedDelta = new JavaElementDelta(element); changedDelta.changed(changeFlag); insertDeltaTree(element, changedDelta); return changedDelta; } /* * Records the last changed AST . */ public void changedAST(CompilationUnit changedAST) { this.ast = changedAST; changed(F_AST_AFFECTED); } /** * Clears the collection of affected children. */ protected void clearAffectedChildren() { this.affectedChildren = EMPTY_DELTA; this.childIndex = null; } /** * Mark this delta as a content changed delta. */ public void contentChanged() { this.changeFlags |= F_CONTENT; } /** * Creates the nested deltas for a closed element. */ public void closed(IJavaElement element) { JavaElementDelta delta = new JavaElementDelta(element); delta.changed(F_CLOSED); insertDeltaTree(element, delta); } /** * Creates the nested delta deltas based on the affected element * its delta, and the root of this delta tree. Returns the root * of the created delta tree. */ protected JavaElementDelta createDeltaTree(IJavaElement element, JavaElementDelta delta) { JavaElementDelta childDelta = delta; ArrayList ancestors= getAncestors(element); if (ancestors == null) { if (equalsAndSameParent(delta.getElement(), getElement())) { // handle case of two jars that can be equals but not in the same project // the element being changed is the root element this.kind= delta.kind; this.changeFlags = delta.changeFlags; this.movedToHandle = delta.movedToHandle; this.movedFromHandle = delta.movedFromHandle; } } else { for (int i = 0, size = ancestors.size(); i < size; i++) { IJavaElement ancestor = (IJavaElement) ancestors.get(i); JavaElementDelta ancestorDelta = new JavaElementDelta(ancestor); ancestorDelta.addAffectedChild(childDelta); childDelta = ancestorDelta; } } return childDelta; } /** * Returns whether the two java elements are equals and have the same parent. */ protected static boolean equalsAndSameParent(IJavaElement e1, IJavaElement e2) { IJavaElement parent1; return e1.equals(e2) && ((parent1 = e1.getParent()) != null) && parent1.equals(e2.getParent()); } /** * Returns the <code>JavaElementDelta</code> for the given element * in the delta tree, or null, if no delta for the given element is found. */ protected JavaElementDelta find(IJavaElement e) { if (equalsAndSameParent(getElement(), e)) // handle case of two jars that can be equals but not in the same project return this; return findDescendant(new Key(e)); } /** * Returns the descendant delta for the given key, or <code>null</code>, * if no delta for the given key is found in the delta tree below this delta. */ protected JavaElementDelta findDescendant(Key key) { if (this.affectedChildren.length == 0) return null; Integer index = getChildIndex(key); if (index != null) return (JavaElementDelta) this.affectedChildren[index]; for (IJavaElementDelta child : this.affectedChildren) { JavaElementDelta delta = ((JavaElementDelta) child).findDescendant(key); if (delta != null) return delta; } return null; } /** * Mark this delta as a fine-grained delta. */ public void fineGrained() { changed(F_FINE_GRAINED); } /** * @see IJavaElementDelta */ public IJavaElementDelta[] getAddedChildren() { return getChildrenOfType(ADDED); } /** * @see IJavaElementDelta */ public IJavaElementDelta[] getAffectedChildren() { return this.affectedChildren; } /** * Returns a collection of all the parents of this element up to (but * not including) the root of this tree in bottom-up order. If the given * element is not a descendant of the root of this tree, <code>null</code> * is returned. */ private ArrayList getAncestors(IJavaElement element) { IJavaElement parent = element.getParent(); if (parent == null) { return null; } ArrayList parents = new ArrayList(); while (!parent.equals(this.changedElement)) { parents.add(parent); parent = parent.getParent(); if (parent == null) { return null; } } parents.trimToSize(); return parents; } public CompilationUnit getCompilationUnitAST() { return this.ast; } public IJavaElementDelta[] getAnnotationDeltas() { return this.annotationDeltas; } /** * @see IJavaElementDelta */ public IJavaElementDelta[] getChangedChildren() { return getChildrenOfType(CHANGED); } /** * Returns the index of the delta in the collection of affected children, * or <code>null</code> if the child delta for the given key is not found. */ protected Integer getChildIndex(Key key) { int length = this.affectedChildren.length; if (length < NEED_CHILD_INDEX) { for (int i = 0; i < length; i++) { if (equalsAndSameParent(key.element, this.affectedChildren[i].getElement())) { return i; } } return null; } if (this.childIndex == null) { this.childIndex = new HashMap<Key, Integer>(); for (int i = 0; i < length; i++) { this.childIndex.put(new Key(this.affectedChildren[i].getElement()), i); } } return this.childIndex.get(key); } /** * @see IJavaElementDelta */ protected IJavaElementDelta[] getChildrenOfType(int type) { int length = this.affectedChildren.length; if (length == 0) { return new IJavaElementDelta[] {}; } ArrayList children= new ArrayList(length); for (int i = 0; i < length; i++) { if (this.affectedChildren[i].getKind() == type) { children.add(this.affectedChildren[i]); } } IJavaElementDelta[] childrenOfType = new IJavaElementDelta[children.size()]; children.toArray(childrenOfType); return childrenOfType; } /** * Returns the delta for a given element. Only looks below this * delta. */ protected JavaElementDelta getDeltaFor(IJavaElement element) { return find(element); } /** * @see IJavaElementDelta */ public IJavaElement getElement() { return this.changedElement; } /** * @see IJavaElementDelta */ public IJavaElement getMovedFromElement() { return this.movedFromHandle; } /** * @see IJavaElementDelta */ public IJavaElement getMovedToElement() { return this.movedToHandle; } /** * @see IJavaElementDelta */ public IJavaElementDelta[] getRemovedChildren() { return getChildrenOfType(REMOVED); } /** * Return the collection of resource deltas. Return null if none. */ public IResourceDelta[] getResourceDeltas() { if (this.resourceDeltas == null) return null; if (this.resourceDeltas.length != this.resourceDeltasCounter) { System.arraycopy(this.resourceDeltas, 0, this.resourceDeltas = new IResourceDelta[this.resourceDeltasCounter], 0, this.resourceDeltasCounter); } return this.resourceDeltas; } /** * Adds the new element to a new array that contains all of the elements of the old array. * Returns the new array. */ protected IJavaElementDelta[] growAndAddToArray(IJavaElementDelta[] array, IJavaElementDelta addition) { IJavaElementDelta[] old = array; array = new IJavaElementDelta[old.length + 1]; System.arraycopy(old, 0, array, 0, old.length); array[old.length] = addition; return array; } /** * Creates the delta tree for the given element and delta, and then * inserts the tree as an affected child of this node. */ protected void insertDeltaTree(IJavaElement element, JavaElementDelta delta) { JavaElementDelta childDelta= createDeltaTree(element, delta); if (!equalsAndSameParent(element, getElement())) { // handle case of two jars that can be equals but not in the same project addAffectedChild(childDelta); } } /** * Creates the nested deltas resulting from an move operation. * Convenience method for creating the "move from" delta. * The constructor should be used to create the root delta * and then the move operation should call this method. */ public void movedFrom(IJavaElement movedFromElement, IJavaElement movedToElement) { JavaElementDelta removedDelta = new JavaElementDelta(movedFromElement); removedDelta.kind = REMOVED; removedDelta.changeFlags |= F_MOVED_TO; removedDelta.movedToHandle = movedToElement; insertDeltaTree(movedFromElement, removedDelta); } /** * Creates the nested deltas resulting from an move operation. * Convenience method for creating the "move to" delta. * The constructor should be used to create the root delta * and then the move operation should call this method. */ public void movedTo(IJavaElement movedToElement, IJavaElement movedFromElement) { JavaElementDelta addedDelta = new JavaElementDelta(movedToElement); addedDelta.kind = ADDED; addedDelta.changeFlags |= F_MOVED_FROM; addedDelta.movedFromHandle = movedFromElement; insertDeltaTree(movedToElement, addedDelta); } /** * Creates the nested deltas for an opened element. */ public void opened(IJavaElement element) { JavaElementDelta delta = new JavaElementDelta(element); delta.changed(F_OPENED); insertDeltaTree(element, delta); } /** * Removes the child delta from the collection of affected children. */ protected void removeAffectedChild(JavaElementDelta child) { if (this.affectedChildren.length == 0) return; Key childKey = new Key(child.getElement()); Integer exisingChildIndex = getChildIndex(childKey); if (exisingChildIndex != null) { removeExistingChild(childKey, exisingChildIndex); } } /** * Removes the element from the array. * Returns the a new array which has shrunk. */ protected IJavaElementDelta[] removeAndShrinkArray(IJavaElementDelta[] old, int index) { IJavaElementDelta[] array = new IJavaElementDelta[old.length - 1]; if (index > 0) System.arraycopy(old, 0, array, 0, index); int rest = old.length - index - 1; if (rest > 0) System.arraycopy(old, index + 1, array, index, rest); return array; } /** * Creates the nested deltas resulting from an delete operation. * Convenience method for creating removed deltas. * The constructor should be used to create the root delta * and then the delete operation should call this method. */ public void removed(IJavaElement element) { removed(element, 0); } public void removed(IJavaElement element, int flags) { JavaElementDelta removedDelta= new JavaElementDelta(element); insertDeltaTree(element, removedDelta); JavaElementDelta actualDelta = getDeltaFor(element); if (actualDelta != null) { actualDelta.removed(); actualDelta.changeFlags |= flags; actualDelta.clearAffectedChildren(); } } /** * Removes the existing child delta from the collection of affected children. */ protected void removeExistingChild(Key key, int index) { this.affectedChildren = removeAndShrinkArray(this.affectedChildren, index); if (this.childIndex != null) { int length = this.affectedChildren.length; if (length < NEED_CHILD_INDEX) this.childIndex = null; else { this.childIndex.remove(key); for (int i = index; i < length; i++) { this.childIndex.put(new Key(this.affectedChildren[i].getElement()), i); } } } } /** * Creates the nested deltas resulting from a change operation. * Convenience method for creating change deltas. * The constructor should be used to create the root delta * and then a change operation should call this method. */ public void sourceAttached(IJavaElement element) { JavaElementDelta attachedDelta = new JavaElementDelta(element); attachedDelta.changed(F_SOURCEATTACHED); insertDeltaTree(element, attachedDelta); } /** * Creates the nested deltas resulting from a change operation. * Convenience method for creating change deltas. * The constructor should be used to create the root delta * and then a change operation should call this method. */ public void sourceDetached(IJavaElement element) { JavaElementDelta detachedDelta = new JavaElementDelta(element); detachedDelta.changed(F_SOURCEDETACHED); insertDeltaTree(element, detachedDelta); } /** * Returns a string representation of this delta's * structure suitable for debug purposes. * * @see #toString() */ public String toDebugString(int depth) { StringBuffer buffer = new StringBuffer(); for (int i= 0; i < depth; i++) { buffer.append('\t'); } buffer.append(((JavaElement)getElement()).toDebugString()); toDebugString(buffer); IJavaElementDelta[] children = getAffectedChildren(); if (children != null) { for (int i = 0; i < children.length; ++i) { buffer.append("\n"); //$NON-NLS-1$ buffer.append(((JavaElementDelta) children[i]).toDebugString(depth + 1)); } } for (int i = 0; i < this.resourceDeltasCounter; i++) { buffer.append("\n");//$NON-NLS-1$ for (int j = 0; j < depth+1; j++) { buffer.append('\t'); } IResourceDelta resourceDelta = this.resourceDeltas[i]; buffer.append(resourceDelta.toString()); buffer.append("["); //$NON-NLS-1$ switch (resourceDelta.getKind()) { case IResourceDelta.ADDED : buffer.append('+'); break; case IResourceDelta.REMOVED : buffer.append('-'); break; case IResourceDelta.CHANGED : buffer.append('*'); break; default : buffer.append('?'); break; } buffer.append("]"); //$NON-NLS-1$ } IJavaElementDelta[] annotations = getAnnotationDeltas(); if (annotations != null) { for (int i = 0; i < annotations.length; ++i) { buffer.append("\n"); //$NON-NLS-1$ buffer.append(((JavaElementDelta) annotations[i]).toDebugString(depth + 1)); } } return buffer.toString(); } protected boolean toDebugString(StringBuffer buffer, int flags) { boolean prev = super.toDebugString(buffer, flags); if ((flags & IJavaElementDelta.F_CHILDREN) != 0) { if (prev) buffer.append(" | "); //$NON-NLS-1$ buffer.append("CHILDREN"); //$NON-NLS-1$ prev = true; } if ((flags & IJavaElementDelta.F_CONTENT) != 0) { if (prev) buffer.append(" | "); //$NON-NLS-1$ buffer.append("CONTENT"); //$NON-NLS-1$ prev = true; } if ((flags & IJavaElementDelta.F_MOVED_FROM) != 0) { if (prev) buffer.append(" | "); //$NON-NLS-1$ buffer.append("MOVED_FROM(" + ((JavaElement)getMovedFromElement()).toStringWithAncestors() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ prev = true; } if ((flags & IJavaElementDelta.F_MOVED_TO) != 0) { if (prev) buffer.append(" | "); //$NON-NLS-1$ buffer.append("MOVED_TO(" + ((JavaElement)getMovedToElement()).toStringWithAncestors() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ prev = true; } if ((flags & IJavaElementDelta.F_ADDED_TO_CLASSPATH) != 0) { if (prev) buffer.append(" | "); //$NON-NLS-1$ buffer.append("ADDED TO CLASSPATH"); //$NON-NLS-1$ prev = true; } if ((flags & IJavaElementDelta.F_REMOVED_FROM_CLASSPATH) != 0) { if (prev) buffer.append(" | "); //$NON-NLS-1$ buffer.append("REMOVED FROM CLASSPATH"); //$NON-NLS-1$ prev = true; } if ((flags & IJavaElementDelta.F_REORDER) != 0) { if (prev) buffer.append(" | "); //$NON-NLS-1$ buffer.append("REORDERED"); //$NON-NLS-1$ prev = true; } if ((flags & IJavaElementDelta.F_ARCHIVE_CONTENT_CHANGED) != 0) { if (prev) buffer.append(" | "); //$NON-NLS-1$ buffer.append("ARCHIVE CONTENT CHANGED"); //$NON-NLS-1$ prev = true; } if ((flags & IJavaElementDelta.F_SOURCEATTACHED) != 0) { if (prev) buffer.append(" | "); //$NON-NLS-1$ buffer.append("SOURCE ATTACHED"); //$NON-NLS-1$ prev = true; } if ((flags & IJavaElementDelta.F_SOURCEDETACHED) != 0) { if (prev) buffer.append(" | "); //$NON-NLS-1$ buffer.append("SOURCE DETACHED"); //$NON-NLS-1$ prev = true; } if ((flags & IJavaElementDelta.F_FINE_GRAINED) != 0) { if (prev) buffer.append(" | "); //$NON-NLS-1$ buffer.append("FINE GRAINED"); //$NON-NLS-1$ prev = true; } if ((flags & IJavaElementDelta.F_PRIMARY_WORKING_COPY) != 0) { if (prev) buffer.append(" | "); //$NON-NLS-1$ buffer.append("PRIMARY WORKING COPY"); //$NON-NLS-1$ prev = true; } if ((flags & IJavaElementDelta.F_CLASSPATH_CHANGED) != 0) { if (prev) buffer.append(" | "); //$NON-NLS-1$ buffer.append("RAW CLASSPATH CHANGED"); //$NON-NLS-1$ prev = true; } if ((flags & IJavaElementDelta.F_RESOLVED_CLASSPATH_CHANGED) != 0) { if (prev) buffer.append(" | "); //$NON-NLS-1$ buffer.append("RESOLVED CLASSPATH CHANGED"); //$NON-NLS-1$ prev = true; } if ((flags & IJavaElementDelta.F_PRIMARY_RESOURCE) != 0) { if (prev) buffer.append(" | "); //$NON-NLS-1$ buffer.append("PRIMARY RESOURCE"); //$NON-NLS-1$ prev = true; } if ((flags & IJavaElementDelta.F_OPENED) != 0) { if (prev) buffer.append(" | "); //$NON-NLS-1$ buffer.append("OPENED"); //$NON-NLS-1$ prev = true; } if ((flags & IJavaElementDelta.F_CLOSED) != 0) { if (prev) buffer.append(" | "); //$NON-NLS-1$ buffer.append("CLOSED"); //$NON-NLS-1$ prev = true; } if ((flags & IJavaElementDelta.F_AST_AFFECTED) != 0) { if (prev) buffer.append(" | "); //$NON-NLS-1$ buffer.append("AST AFFECTED"); //$NON-NLS-1$ prev = true; } if ((flags & IJavaElementDelta.F_CATEGORIES) != 0) { if (prev) buffer.append(" | "); //$NON-NLS-1$ buffer.append("CATEGORIES"); //$NON-NLS-1$ prev = true; } if ((flags & IJavaElementDelta.F_ANNOTATIONS) != 0) { if (prev) buffer.append(" | "); //$NON-NLS-1$ buffer.append("ANNOTATIONS"); //$NON-NLS-1$ prev = true; } return prev; } /** * Returns a string representation of this delta's * structure suitable for debug purposes. */ public String toString() { return toDebugString(0); } }