| /******************************************************************************* |
| * Copyright (c) 2007, 2010 Oracle. 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: |
| * Oracle - initial API and implementation |
| ******************************************************************************/ |
| package org.eclipse.jpt.common.utility.internal.node; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.ListIterator; |
| import java.util.Set; |
| import java.util.Vector; |
| |
| import org.eclipse.jpt.common.utility.internal.iterators.CloneIterator; |
| import org.eclipse.jpt.common.utility.internal.iterators.CloneListIterator; |
| import org.eclipse.jpt.common.utility.internal.iterators.FilteringIterator; |
| import org.eclipse.jpt.common.utility.internal.model.AbstractModel; |
| import org.eclipse.jpt.common.utility.internal.model.AspectChangeSupport; |
| import org.eclipse.jpt.common.utility.internal.model.ChangeSupport; |
| |
| /** |
| * Base class for Node classes. |
| * Provides support for the following: |
| * initialization |
| * enforced object identity wrt #equals()/#hashCode() |
| * containment hierarchy (parent/child) |
| * user comment |
| * dirty flag |
| * problems |
| * sorting |
| * |
| * Typically, subclasses should consider implementing the following methods: |
| * the appropriate constructors |
| * (with the appropriately-restrictive type declaration for parent) |
| * #initialize() |
| * #initialize(Node parentNode) |
| * #checkParent(Node parentNode) |
| * #addChildrenTo(List list) |
| * #nodeRemoved(Node) |
| * #validator() |
| * #transientAspectNames() or |
| * #addTransientAspectNamesTo(Set transientAspectNames) |
| * #addProblemsTo(List currentProblems) |
| * #nonValidatedAspectNames() |
| * #addNonValidatedAspectNamesTo(Set nonValidatedAspectNames) |
| * #displayString() |
| * #toString(StringBuilder sb) |
| */ |
| public abstract class AbstractNode |
| extends AbstractModel |
| implements Node |
| { |
| |
| /** Containment hierarchy. */ |
| private Node parent; // pseudo-final |
| |
| /** Track whether the node has changed. */ |
| private volatile boolean dirty; |
| private volatile boolean dirtyBranch; |
| |
| /** |
| * The node's problems, as calculated during validation. |
| * This list should only be modified via a ProblemSynchronizer, |
| * allowing for asynchronous modification from another thread. |
| */ |
| private Vector<Problem> problems; // pseudo-final |
| private static final Object[] EMPTY_PROBLEM_MESSAGE_ARGUMENTS = new Object[0]; |
| |
| /** |
| * Cache the node's "branch" problems, as calculated during validation. |
| * This list should only be modified via a ProblemSynchronizer, |
| * allowing for asynchronous modification from another thread. |
| * This must be recalculated every time this node or one of its |
| * descendants changes it problems. |
| */ |
| private Vector<Problem> branchProblems; // pseudo-final |
| |
| /** User comment. */ |
| private volatile String comment; |
| |
| |
| // ********** static fields ********** |
| |
| /** |
| * Sets of transient aspect names, keyed by class. |
| * This is built up lazily, as the objects are modified. |
| */ |
| private static final HashMap<Class<? extends AbstractNode>, HashSet<String>> transientAspectNameSets = new HashMap<Class<? extends AbstractNode>, HashSet<String>>(); |
| |
| /** |
| * Sets of non-validated aspect names, keyed by class. |
| * This is built up lazily, as the objects are modified. |
| */ |
| private static final HashMap<Class<? extends AbstractNode>, HashSet<String>> nonValidatedAspectNameSets = new HashMap<Class<? extends AbstractNode>, HashSet<String>>(); |
| |
| |
| // ********** constructors ********** |
| |
| /** |
| * Most objects must have a parent. |
| * Use this constructor to create a new node. |
| * @see #initialize(Node) |
| */ |
| protected AbstractNode(Node parent) { |
| super(); |
| this.initialize(); |
| this.initialize(parent); |
| } |
| |
| |
| // ********** initialization ********** |
| |
| /** |
| * Initialize a newly-created instance. |
| * @see #initialize(Node) |
| */ |
| protected void initialize() { |
| this.comment = ""; //$NON-NLS-1$ |
| |
| // a new object is dirty, by definition |
| this.dirty = true; |
| this.dirtyBranch = true; |
| |
| this.problems = new Vector<Problem>(); |
| this.branchProblems = new Vector<Problem>(); |
| |
| // when you override this method, don't forget to include: |
| // super.initialize(); |
| } |
| |
| /** |
| * Initialize a newly-created instance. |
| * @see #initialize() |
| */ |
| protected void initialize(Node parentNode) { |
| this.checkParent(parentNode); |
| this.parent = parentNode; |
| // when you override this method, don't forget to include: |
| // super.initialize(parentNode); |
| } |
| |
| @Override |
| protected ChangeSupport buildChangeSupport() { |
| return new AspectChangeSupport(this, this.buildChangeSupportListener()); |
| } |
| |
| protected AspectChangeSupport.Listener buildChangeSupportListener() { |
| return new AspectChangeSupport.Listener() { |
| public void aspectChanged(String aspectName) { |
| AbstractNode.this.aspectChanged(aspectName); |
| } |
| }; |
| } |
| |
| |
| // ********** equality ********** |
| |
| /** |
| * Enforce object identity - do not allow objects to be equal unless |
| * they are the same object. |
| * Do NOT override this method - we rely on object identity extensively. |
| */ |
| @Override |
| public final boolean equals(Object o) { |
| return this == o; |
| } |
| |
| /** |
| * Enforce object identity - do not allow objects to be equal unless |
| * they are the same object. |
| * Do NOT override this method - we rely on object identity extensively. |
| */ |
| @Override |
| public final int hashCode() { |
| return super.hashCode(); |
| } |
| |
| |
| // ********** containment hierarchy (parent/children) ********** |
| |
| /** |
| * INTRA-TREE API? |
| * Return the node's parent in the containment hierarchy. |
| * Most nodes must have a parent. |
| * @see #children() |
| */ |
| public Node getParent() { |
| return this.parent; |
| } |
| |
| /** |
| * Throw an IllegalArgumentException if the parent is not valid |
| * for the node. |
| * By default require a non-null parent. Override if other restrictions exist |
| * or the parent should be null. |
| * NB: Root node model implementations will need to override this method. |
| */ |
| protected void checkParent(Node parentNode) { |
| if (parentNode == null) { |
| throw new IllegalArgumentException("The parent node cannot be null"); //$NON-NLS-1$ |
| } |
| } |
| |
| /** |
| * INTRA-TREE API? |
| * Return the node's children, which are also nodes. |
| * Do NOT override this method. |
| * Override #addChildrenTo(List). |
| * @see #getParent() |
| * @see #addChildrenTo(java.util.List) |
| */ |
| public final Iterator<Node> children() { |
| List<Node> children = new ArrayList<Node>(); |
| this.addChildrenTo(children); |
| return children.iterator(); |
| } |
| |
| /** |
| * Subclasses should override this method to add their children |
| * to the specified list. |
| * @see #children() |
| */ |
| protected void addChildrenTo(@SuppressWarnings("unused") List<Node> list) { |
| // this class has no children, subclasses will... |
| // when you override this method, don't forget to include: |
| // super.addChildrenTo(list); |
| } |
| |
| /** |
| * INTRA-TREE API? |
| * Return the containment hierarchy's root node. |
| * Most nodes must have a root. |
| * @see #getParent() |
| * NB: Assume the root has no parent. |
| */ |
| public Node root() { |
| Node p = this.parent; |
| return (p == null) ? this : p.root(); |
| } |
| |
| /** |
| * Return whether the node is a descendant of the specified node. |
| * By definition, a node is a descendant of itself. |
| */ |
| public boolean isDescendantOf(Node node) { |
| return (this == node) || this.parentIsDescendantOf(node); |
| } |
| |
| protected boolean parentIsDescendantOf(Node node) { |
| return (this.parent != null) && this.parent.isDescendantOf(node); |
| } |
| |
| /** |
| * Return a collection holding all the node's "references", and all |
| * the node's descendants' "references". "References" are |
| * objects that are "referenced" by another object, as opposed |
| * to "owned" by another object. |
| */ |
| public Iterator<Node.Reference> branchReferences() { |
| Collection<Node.Reference> branchReferences = new ArrayList<Node.Reference>(1000); // start big |
| this.addBranchReferencesTo(branchReferences); |
| return branchReferences.iterator(); |
| } |
| |
| /** |
| * INTRA-TREE API |
| * Add the node's "references", and all the node's descendants' |
| * "references", to the specified collection. "References" are |
| * objects that are "referenced" by another object, as opposed |
| * to "owned" by another object. |
| * This method is of particular concern to Handles, since most |
| * (hopefully all) "references" are held by Handles. |
| * @see Reference |
| * @see #children() |
| */ |
| public void addBranchReferencesTo(Collection<Node.Reference> branchReferences) { |
| for (Iterator<Node> stream = this.children(); stream.hasNext(); ) { |
| Node child = stream.next(); // pull out the child to ease debugging |
| child.addBranchReferencesTo(branchReferences); |
| } |
| } |
| |
| /** |
| * Return all the nodes in the object's branch of the tree, |
| * including the node itself. The nodes will probably returned |
| * in "depth-first" order. |
| * Only really used for testing and debugging. |
| */ |
| public Iterator<Node> allNodes() { |
| Collection<Node> nodes = new ArrayList<Node>(1000); // start big |
| this.addAllNodesTo(nodes); |
| return nodes.iterator(); |
| } |
| |
| /** |
| * INTRA-TREE API? |
| * Add all the nodes in the object's branch of the tree, |
| * including the node itself, to the specified collection. |
| * Only really used for testing and debugging. |
| */ |
| public void addAllNodesTo(Collection<Node> nodes) { |
| nodes.add(this); |
| for (Iterator<Node> stream = this.children(); stream.hasNext(); ) { |
| Node child = stream.next(); // pull out the child to ease debugging |
| child.addAllNodesTo(nodes); |
| } |
| } |
| |
| |
| // ********** model synchronization support ********** |
| |
| /** |
| * INTRA-TREE API |
| * This is a general notification that the specified node has been |
| * removed from the tree. The node receiving this notification |
| * should perform any necessary updates to remain in synch |
| * with the tree (e.g. clearing out or replacing any references |
| * to the removed node or any of the removed node's descendants). |
| * @see #isDescendantOf(Node) |
| */ |
| public void nodeRemoved(Node node) { |
| for (Iterator<Node> stream = this.children(); stream.hasNext(); ) { |
| Node child = stream.next(); // pull out the child to ease debugging |
| child.nodeRemoved(node); |
| } |
| // when you override this method, don't forget to include: |
| // super.nodeRemoved(node); |
| } |
| |
| /** |
| * convenience method |
| * return whether node1 is a descendant of node2; |
| * node1 can be null |
| */ |
| protected boolean nodeIsDescendantOf(Node node1, Node node2) { |
| return (node1 != null) && node1.isDescendantOf(node2); |
| } |
| |
| /** |
| * INTRA-TREE API |
| * This is a general notification that the specified node has been |
| * renamed. The node receiving this notification should mark its |
| * branch dirty if necessary (i.e. it references the renamed node |
| * or one of its descendants). This method is of particular concern |
| * to Handles. |
| * @see #isDescendantOf(Node) |
| */ |
| public void nodeRenamed(Node node) { |
| for (Iterator<Node> stream = this.children(); stream.hasNext(); ) { |
| Node child = stream.next(); // pull out the child to ease debugging |
| child.nodeRenamed(node); |
| } |
| // when you override this method, don't forget to include: |
| // super.nodeRenamed(node); |
| } |
| |
| |
| // ********** user comment ********** |
| |
| /** |
| * Return the object's user comment. |
| */ |
| public final String comment() { |
| return this.comment; |
| } |
| |
| /** |
| * Set the object's user comment. |
| */ |
| public final void setComment(String comment) { |
| Object old = this.comment; |
| this.comment = comment; |
| this.firePropertyChanged(COMMENT_PROPERTY, old, comment); |
| } |
| |
| |
| // ********** change support ********** |
| |
| /** |
| * An aspect of the node has changed: |
| * - if it is a persistent aspect, mark the object dirty |
| * - if it is a significant aspect, validate the object |
| */ |
| protected void aspectChanged(String aspectName) { |
| if (this.aspectIsPersistent(aspectName)) { |
| // System.out.println(Thread.currentThread() + " dirty change: " + this + ": " + aspectName); |
| this.markDirty(); |
| } |
| if (this.aspectChangeRequiresValidation(aspectName)) { |
| // System.out.println(Thread.currentThread() + " validation change: " + this + ": " + aspectName); |
| this.validate(); |
| } |
| } |
| |
| protected void validate() { |
| this.getValidator().validate(); |
| } |
| |
| /** |
| * INTRA-TREE API |
| * Return a validator that will be invoked whenever a |
| * "validated" aspect of the node tree changes. |
| * Typically only the root node directly holds a validator. |
| * NB: Root node model implementations will need to override this method. |
| */ |
| public Node.Validator getValidator() { |
| if (this.parent == null) { |
| throw new IllegalStateException("This node should not be firing change events during its construction."); //$NON-NLS-1$ |
| } |
| return this.parent.getValidator(); |
| } |
| |
| /** |
| * Set a validator that will be invoked whenever a |
| * "validated" aspect of the node tree changes. |
| * Typically only the root node directly holds a validator. |
| * NB: Root node model implementations will need to override this method. |
| */ |
| public void setValidator(Node.Validator validator) { |
| if (this.parent == null) { |
| throw new IllegalStateException("This root node should implement #setValidator(Node.Validator)."); //$NON-NLS-1$ |
| } |
| throw new UnsupportedOperationException("Only root nodes implement #setValidator(Node.Validator)."); //$NON-NLS-1$ |
| } |
| |
| |
| // ********** dirty flag support ********** |
| |
| /** |
| * Return whether any persistent aspects of the object |
| * have changed since the object was last read or saved. |
| * This does NOT include changes to the object's descendants. |
| */ |
| public final boolean isDirty() { |
| return this.dirty; |
| } |
| |
| /** |
| * Return whether any persistent aspects of the object, |
| * or any of its descendants, have changed since the object and |
| * its descendants were last read or saved. |
| */ |
| public final boolean isDirtyBranch() { |
| return this.dirtyBranch; |
| } |
| |
| /** |
| * Return whether the object is unmodified |
| * since it was last read or saved. |
| * This does NOT include changes to the object's descendants. |
| */ |
| public final boolean isClean() { |
| return ! this.dirty; |
| } |
| |
| /** |
| * Return whether the object and all of its descendants |
| * are unmodified since the object and |
| * its descendants were last read or saved. |
| */ |
| public final boolean isCleanBranch() { |
| return ! this.dirtyBranch; |
| } |
| |
| /** |
| * Set the dirty branch flag setting. This is set to true |
| * when either the object or one of its descendants becomes dirty. |
| */ |
| private void setIsDirtyBranch(boolean dirtyBranch) { |
| boolean old = this.dirtyBranch; |
| this.dirtyBranch = dirtyBranch; |
| this.firePropertyChanged(DIRTY_BRANCH_PROPERTY, old, dirtyBranch); |
| } |
| |
| /** |
| * Mark the object as dirty and as a dirty branch. |
| * An object is marked dirty when either a "persistent" attribute |
| * has changed or its save location has changed. |
| */ |
| private void markDirty() { |
| this.dirty = true; |
| this.markBranchDirty(); |
| } |
| |
| /** |
| * INTRA-TREE API |
| * Mark the node and its parent as dirty branches. |
| * This message is propagated up the containment |
| * tree when a particular node becomes dirty. |
| */ |
| public void markBranchDirty() { |
| // short-circuit any unnecessary propagation |
| if (this.dirtyBranch) { |
| // if this is already a dirty branch, the parent must be also |
| return; |
| } |
| |
| this.setIsDirtyBranch(true); |
| this.markParentBranchDirty(); |
| } |
| |
| protected void markParentBranchDirty() { |
| if (this.parent != null) { |
| this.parent.markBranchDirty(); |
| } |
| } |
| |
| /** |
| * Mark the object and all its descendants as dirty. |
| * This is used when the save location of some |
| * top-level object is changed and the entire |
| * containment tree must be marked dirty so it |
| * will be written out. |
| */ |
| public final void markEntireBranchDirty() { |
| this.markDirty(); |
| for (Iterator<Node> stream = this.children(); stream.hasNext(); ) { |
| Node child = stream.next(); // pull out the child to ease debugging |
| child.markEntireBranchDirty(); |
| } |
| } |
| |
| /** |
| * Mark the object and all its descendants as clean. |
| * Then notify the object's parent that it (the parent) |
| * might now be a clean branch also. |
| * Typically used when the object has just been |
| * read in or written out. |
| */ |
| public final void markEntireBranchClean() { |
| this.cascadeMarkEntireBranchClean(); |
| this.markParentBranchCleanIfPossible(); |
| } |
| |
| protected void markParentBranchCleanIfPossible() { |
| if (this.parent != null) { |
| this.parent.markBranchCleanIfPossible(); |
| } |
| } |
| |
| /** |
| * INTRA-TREE API |
| * Mark the node and all its descendants as clean. |
| * Typically used when the node has just been |
| * read in or written out. |
| * This method is for internal use only; it is not for |
| * client use. |
| * Not the best of method names.... :-( |
| */ |
| public final void cascadeMarkEntireBranchClean() { |
| for (Iterator<Node> stream = this.children(); stream.hasNext(); ) { |
| Node child = stream.next(); // pull out the child to ease debugging |
| child.cascadeMarkEntireBranchClean(); |
| } |
| this.dirty = false; |
| this.setIsDirtyBranch(false); |
| } |
| |
| /** |
| * INTRA-TREE API |
| * A child node's branch has been marked clean. If the node |
| * itself is clean and if all of its children are also clean, the |
| * node's branch can be marked clean. Then, if the node's |
| * branch is clean, the node will notify its parent that it might |
| * be clean also. This message is propagated up the containment |
| * tree when a particular node becomes clean. |
| */ |
| public final void markBranchCleanIfPossible() { |
| // short-circuit any unnecessary propagation |
| if (this.dirty) { |
| // if the object is "locally" dirty, it is still a dirty branch |
| return; |
| } |
| |
| for (Iterator<Node> stream = this.children(); stream.hasNext(); ) { |
| Node child = stream.next(); // pull out the child to ease debugging |
| if (child.isDirtyBranch()) { |
| return; |
| } |
| } |
| |
| this.setIsDirtyBranch(false); |
| this.markParentBranchCleanIfPossible(); |
| } |
| |
| private boolean aspectIsPersistent(String aspectName) { |
| return ! this.aspectIsTransient(aspectName); |
| } |
| |
| private boolean aspectIsTransient(String aspectName) { |
| return this.transientAspectNames().contains(aspectName); |
| } |
| |
| /** |
| * Return a set of the object's transient aspect names. |
| * These are the aspects that, when they change, will NOT cause the |
| * object to be marked dirty. |
| * If you need instance-based calculation of your transient aspects, |
| * override this method. If class-based calculation is sufficient, |
| * override #addTransientAspectNamesTo(Set). |
| */ |
| protected final Set<String> transientAspectNames() { |
| synchronized (transientAspectNameSets) { |
| HashSet<String> transientAspectNames = transientAspectNameSets.get(this.getClass()); |
| if (transientAspectNames == null) { |
| transientAspectNames = new HashSet<String>(); |
| this.addTransientAspectNamesTo(transientAspectNames); |
| transientAspectNameSets.put(this.getClass(), transientAspectNames); |
| } |
| return transientAspectNames; |
| } |
| } |
| |
| /** |
| * Add the object's transient aspect names to the specified set. |
| * These are the aspects that, when they change, will NOT cause the |
| * object to be marked dirty. |
| * If class-based calculation of your transient aspects is sufficient, |
| * override this method. If you need instance-based calculation, |
| * override #transientAspectNames(). |
| */ |
| protected void addTransientAspectNamesTo(Set<String> transientAspectNames) { |
| transientAspectNames.add(DIRTY_BRANCH_PROPERTY); |
| transientAspectNames.add(BRANCH_PROBLEMS_LIST); |
| transientAspectNames.add(HAS_BRANCH_PROBLEMS_PROPERTY); |
| // when you override this method, don't forget to include: |
| // super.addTransientAspectNamesTo(transientAspectNames); |
| } |
| |
| /** |
| * Return the dirty nodes in the object's branch of the tree, |
| * including the node itself (if appropriate). |
| * Only really used for testing and debugging. |
| */ |
| public final Iterator<Node> allDirtyNodes() { |
| return new FilteringIterator<Node>(this.allNodes()) { |
| @Override |
| protected boolean accept(Node node) { |
| return (node instanceof AbstractNode) && ((AbstractNode) node).isDirty(); |
| } |
| }; |
| } |
| |
| |
| // ********** problems ********** |
| |
| /** |
| * Return the node's problems. |
| * This does NOT include the problems of the node's descendants. |
| * @see #branchProblems() |
| */ |
| public final Iterator<Problem> problems() { |
| return new CloneIterator<Problem>(this.problems); // removes are not allowed |
| } |
| |
| /** |
| * Return the size of the node's problems. |
| * This does NOT include the problems of the node's descendants. |
| * @see #branchProblemsSize() |
| */ |
| public final int problemsSize() { |
| return this.problems.size(); |
| } |
| |
| /** |
| * Return whether the node has problems |
| * This does NOT include the problems of the node's descendants. |
| * @see #hasBranchProblems() |
| */ |
| public final boolean hasProblems() { |
| return ! this.problems.isEmpty(); |
| } |
| |
| /** |
| * Return all the node's problems along with all the |
| * node's descendants' problems. |
| */ |
| public final ListIterator<Problem> branchProblems() { |
| return new CloneListIterator<Problem>(this.branchProblems); // removes are not allowed |
| } |
| |
| /** |
| * Return the size of all the node's problems along with all the |
| * node's descendants' problems. |
| */ |
| public final int branchProblemsSize() { |
| return this.branchProblems.size(); |
| } |
| |
| /** |
| * Return whether the node or any of its descendants have problems. |
| */ |
| public final boolean hasBranchProblems() { |
| return ! this.branchProblems.isEmpty(); |
| } |
| |
| public final boolean containsBranchProblem(Problem problem) { |
| return this.branchProblems.contains(problem); |
| } |
| |
| protected final Problem buildProblem(String messageKey, int messageType, Object... messageArguments) { |
| return new DefaultProblem(this, messageKey, messageType, messageArguments); |
| } |
| |
| protected final Problem buildProblem(String messageKey, int messageType) { |
| return this.buildProblem(messageKey, messageType, EMPTY_PROBLEM_MESSAGE_ARGUMENTS); |
| } |
| |
| /** |
| * Validate the node and all of its descendants, |
| * and update their sets of "branch" problems. |
| * If the node's "branch" problems have changed, |
| * notify the node's parent. |
| */ |
| public void validateBranch() { |
| if (this.validateBranchInternal()) { |
| // if our "branch" problems have changed, then |
| // our parent must rebuild its "branch" problems also |
| this.rebuildParentBranchProblems(); |
| } |
| } |
| |
| protected void rebuildParentBranchProblems() { |
| if (this.parent != null) { |
| this.parent.rebuildBranchProblems(); |
| } |
| } |
| |
| /** |
| * INTRA-TREE API |
| * Validate the node and all of its descendants, |
| * and update their sets of "branch" problems. |
| * Return true if the collection of "branch" problems has changed. |
| * This method is for internal use only; it is not for |
| * client use. |
| */ |
| public boolean validateBranchInternal() { |
| // rebuild "branch" problems in children first |
| for (Iterator<Node> stream = this.children(); stream.hasNext(); ) { |
| Node child = stream.next(); // pull out the child to ease debugging |
| // ignore the return value because we are going to rebuild our "branch" |
| // problems no matter what, to see if they have changed |
| child.validateBranchInternal(); |
| } |
| |
| this.problems.clear(); |
| this.addProblemsTo(this.problems); |
| |
| return this.checkBranchProblems(); |
| } |
| |
| /** |
| * Check for any problems and add them to the specified list. |
| * This method should ONLY add problems for this particular node; |
| * it should NOT add problems for any of this node's descendants |
| * or ancestors. (Although there will be times when it is debatable |
| * as to which node a problem "belongs" to....) |
| * |
| * NB: This method should NOT modify ANY part of the node's state! |
| * It is a READ-ONLY behavior. ONLY the list of current problems |
| * passed in to the method should be modified. |
| */ |
| protected void addProblemsTo(@SuppressWarnings("unused") List<Problem> currentProblems) { |
| // The default is to do nothing. |
| // When you override this method, don't forget to include: |
| // super.addProblemsTo(currentProblems); |
| } |
| |
| /** |
| * Rebuild the "branch" problems and return whether they have |
| * changed. |
| * NB: The entire collection of "branch" problems must be re-calculated |
| * with EVERY "significant" change - we cannot keep it in synch via |
| * change notifications because if a descendant with problems is |
| * removed or replaced we will not receive notification that its |
| * problems were removed from our "branch" problems. |
| */ |
| private boolean checkBranchProblems() { |
| Vector<Problem> oldBranchProblems = new Vector<Problem>(this.branchProblems); |
| int oldSize = this.branchProblems.size(); |
| |
| this.branchProblems.clear(); |
| this.branchProblems.addAll(this.problems); |
| for (Iterator<Node> stream = this.children(); stream.hasNext(); ) { |
| Node child = stream.next(); // pull out the child to ease debugging |
| child.addBranchProblemsTo(this.branchProblems); |
| } |
| |
| // if the size has changed to or from zero, our virtual flag has changed |
| int newSize = this.branchProblems.size(); |
| if ((oldSize == 0) && (newSize != 0)) { |
| this.firePropertyChanged(HAS_BRANCH_PROBLEMS_PROPERTY, false, true); |
| } else if ((oldSize != 0) && (newSize == 0)) { |
| this.firePropertyChanged(HAS_BRANCH_PROBLEMS_PROPERTY, true, false); |
| } |
| |
| if (oldBranchProblems.equals(this.branchProblems)) { |
| return false; // our "branch" problems did not change |
| } |
| // our "branch" problems changed |
| this.fireListChanged(BRANCH_PROBLEMS_LIST, this.branchProblems); |
| return true; |
| } |
| |
| /** |
| * INTRA-TREE API |
| * Add all the problems of the node and all |
| * the problems of its descendants to the |
| * specified collection. |
| */ |
| public final void addBranchProblemsTo(List<Problem> list) { |
| list.addAll(this.branchProblems); |
| } |
| |
| /** |
| * INTRA-TREE API |
| * A child node's "branch" problems changed; |
| * therefore the node's "branch" problems have changed also and |
| * must be rebuilt. |
| */ |
| public final void rebuildBranchProblems() { |
| if ( ! this.checkBranchProblems()) { |
| throw new IllegalStateException("we should not get here unless our \"branch\" problems have changed"); //$NON-NLS-1$ |
| } |
| this.rebuildParentBranchProblems(); |
| } |
| |
| /** |
| * Clear the node's "branch" problems and the "branch" |
| * problems of all of its descendants. |
| * If the node's "branch" problems have changed, |
| * notify the node's parent. |
| */ |
| public final void clearAllBranchProblems() { |
| if (this.clearAllBranchProblemsInternal()) { |
| // if our "branch" problems have changed, then |
| // our parent must rebuild its "branch" problems also |
| this.rebuildParentBranchProblems(); |
| } |
| } |
| |
| /** |
| * INTRA-TREE API |
| * Clear the node's "branch" problems and the "branch" |
| * problems of all of its descendants. |
| * Return true if the collection of "branch" problems has changed. |
| * This method is for internal use only; it is not for |
| * client use. |
| */ |
| public final boolean clearAllBranchProblemsInternal() { |
| if (this.branchProblems.isEmpty()) { |
| return false; |
| } |
| for (Iterator<Node> stream = this.children(); stream.hasNext(); ) { |
| Node child = stream.next(); // pull out the child to ease debugging |
| // ignore the return value because we are going to clear our "branch" |
| // problems no matter what |
| child.clearAllBranchProblemsInternal(); |
| } |
| this.problems.clear(); |
| this.branchProblems.clear(); |
| this.firePropertyChanged(HAS_BRANCH_PROBLEMS_PROPERTY, true, false); |
| this.fireListChanged(BRANCH_PROBLEMS_LIST, this.branchProblems); |
| return true; |
| } |
| |
| /** |
| * Return whether a change to specified aspect requires a re-validation |
| * of the node's tree. |
| */ |
| private boolean aspectChangeRequiresValidation(String aspectName) { |
| return ! this.aspectChangeDoesNotRequireValidation(aspectName); |
| } |
| |
| private boolean aspectChangeDoesNotRequireValidation(String aspectName) { |
| return this.nonValidatedAspectNames().contains(aspectName); |
| } |
| |
| /** |
| * Return a set of the object's "non-validated" aspect names. |
| * These are the aspects that, when they change, will NOT cause the |
| * object (or its containing tree) to be validated, i.e. checked for problems. |
| * If you need instance-based calculation of your "non-validated" aspects, |
| * override this method. If class-based calculation is sufficient, |
| * override #addNonValidatedAspectNamesTo(Set). |
| */ |
| protected final Set<String> nonValidatedAspectNames() { |
| synchronized (nonValidatedAspectNameSets) { |
| HashSet<String> nonValidatedAspectNames = nonValidatedAspectNameSets.get(this.getClass()); |
| if (nonValidatedAspectNames == null) { |
| nonValidatedAspectNames = new HashSet<String>(); |
| this.addNonValidatedAspectNamesTo(nonValidatedAspectNames); |
| nonValidatedAspectNameSets.put(this.getClass(), nonValidatedAspectNames); |
| } |
| return nonValidatedAspectNames; |
| } |
| } |
| |
| /** |
| * Add the object's "non-validated" aspect names to the specified set. |
| * These are the aspects that, when they change, will NOT cause the |
| * object (or its containing tree) to be validated, i.e. checked for problems. |
| * If class-based calculation of your "non-validated" aspects is sufficient, |
| * override this method. If you need instance-based calculation, |
| * override #nonValidatedAspectNames(). |
| */ |
| protected void addNonValidatedAspectNamesTo(Set<String> nonValidatedAspectNames) { |
| nonValidatedAspectNames.add(COMMENT_PROPERTY); |
| nonValidatedAspectNames.add(DIRTY_BRANCH_PROPERTY); |
| nonValidatedAspectNames.add(BRANCH_PROBLEMS_LIST); |
| nonValidatedAspectNames.add(HAS_BRANCH_PROBLEMS_PROPERTY); |
| // when you override this method, don't forget to include: |
| // super.addNonValidatedAspectNamesTo(nonValidatedAspectNames); |
| } |
| |
| |
| // ********** display methods ********** |
| |
| /** |
| * Return a developer-friendly String. If you want something useful for |
| * displaying in a user interface, use #displayString(). |
| * If you want to give more information in your #toString(), |
| * override #toString(StringBuilder sb). |
| * Whatever you add to that string buffer will show up between the parentheses. |
| * @see AbstractModel#toString(StringBuilder sb) |
| * @see #displayString() |
| */ |
| @Override |
| public final String toString() { |
| return super.toString(); |
| } |
| |
| } |