/* * Sun Public License Notice * * The contents of this file are subject to the Sun Public License * Version 1.0 (the "License"). You may not use this file except in * compliance with the License. A copy of the License is available at * http://www.sun.com/ * * The Original Code is NetBeans. The Initial Developer of the Original * Code is Sun Microsystems, Inc. Portions Copyright 1997-2003 Sun * Microsystems, Inc. All Rights Reserved. */ package org.openide.nodes; import java.lang.ref.*; import java.beans.*; import java.util.*; import org.openide.util.Mutex; import org.openide.util.enums.ArrayEnumeration; /** Container for array of nodes. * Can be {@link Node#Node associated} with a node and then * all children in the array have that node set as a parent, and this list * will be returned as the node's children. * * <p>Probably you want to subclass {@link Children.Keys}. * Subclassing <code>Children</code> directly is not recommended. * * @author Jaroslav Tulach */ public abstract class Children extends Object { /** A package internal accessor object to provide priviledged * access to children. */ static final Mutex.Privileged PR = new Mutex.Privileged (); /** Lock for access to hierarchy of all node lists. * Anyone who needs to ensure that there will not * be shared accesses to hierarchy nodes can use this * mutex. * <P> * All operations on the hierarchy of nodes (add, remove, etc.) are * done in the {@link Mutex#writeAccess} method of this lock, so if someone * needs for a certain amount of time to forbid modification, * he can execute his code in {@link Mutex#readAccess}. */ public static final Mutex MUTEX = new Mutex (PR); /** The object representing an empty set of children. Should * be used to represent the children of leaf nodes. The same * object may be used by all such nodes. */ public static final Children LEAF = new Empty (); /** parent node for all nodes in this list (can be null) */ private Node parent; /** mapping from entries to info about them (Entry, Info) */ private java.util.Map map; /** collection of all entries */ private Collection entries = Collections.EMPTY_LIST; /** array of children Reference (ChildrenArray) */ Reference array = new WeakReference (null); /* private StringBuffer debug = new StringBuffer (); private void printStackTrace() { Exception e = new Exception (); java.io.StringWriter w1 = new java.io.StringWriter (); java.io.PrintWriter w = new java.io.PrintWriter (w1); e.printStackTrace(w); w.close (); debug.append (w1.toString ()); debug.append ('\n'); } */ /** Constructor. */ public Children () { } /** Setter of parent node for this list of children. Each children in the list * will have this node set as parent. The parent node will return nodes in * this list as its children. * <P> * This method is called from the Node constructor * * @param n node to attach to * @exception IllegalStateException when this object is already used with * different node */ final void attachTo (final Node n) throws IllegalStateException { // special treatment for LEAF object. if (this == LEAF) { // do not attaches the node because the LEAF cannot have children // and that is why it need not set parent node for them return; } synchronized (this) { if (parent != null) { // already used throw new IllegalStateException ("An instance of Children may not be used for more than one parent node."); // NOI18N } // attach itself as a node list for given node parent = n; } // this is the only place where parent is changed, // but only under readAccess => double check if // it happened correctly try { PR.enterReadAccess (); Node[] nodes = testNodes (); if (nodes == null) return; // fire the change for (int i = 0; i < nodes.length; i++) { Node node = nodes[i]; node.assignTo (Children.this, i); node.fireParentNodeChange (null, parent); } } finally { PR.exitReadAccess (); } } /** Called when the node changes it's children to different nodes. * * @param n node to detach from * @exception IllegalStateException if children were already detached */ final void detachFrom () { // special treatment for LEAF object. if (this == LEAF) { // no need to do anything return; } Node oldParent = null; synchronized (this) { if (parent == null) { // already detached throw new IllegalStateException ("Trying to detach children which do not have parent"); // NOI18N } // remember old parent oldParent = parent; // attach itself as a node list for given node parent = null; } // this is the only place where parent is changed, // but only under readAccess => double check if // it happened correctly try { PR.enterReadAccess (); Node[] nodes = testNodes (); if (nodes == null) return; // fire the change for (int i = 0; i < nodes.length; i++) { Node node = nodes[i]; node.deassignFrom (Children.this); node.fireParentNodeChange (oldParent, null); } } finally { PR.exitReadAccess (); } } /** Get the parent node of these children. * @return the node attached to this children object, or <code>null</code> if there is none yet */ protected final Node getNode () { return parent; } /** Allows access to the clone method for Node. * @return cloned hierarchy * @exception CloneNotSupportedException if not supported */ final Object cloneHierarchy () throws CloneNotSupportedException { return clone (); } /** Handles cloning in the right way, that can be later extended by * subclasses. Of course each subclass that wishes to support cloning * must implement the <code>Cloneable</code> interface, otherwise this method throws * <code>CloneNotSupportedException</code>. * * @return cloned version of this object, with the same class, uninitialized and without * a parent node * *exception CloneNotSupportedException if <code>Cloneable</code> interface is not implemented */ protected Object clone () throws CloneNotSupportedException { Children ch = (Children)super.clone (); ch.parent = null; ch.map = null; ch.entries = Collections.EMPTY_LIST; ch.array = new WeakReference (null); return ch; } /** Add nodes this container. * The parent node of these nodes * is changed to the parent node of this list. Each node can be added * only once. If there is some reason a node cannot be added, for example * if the node expects only a special type of subnodes, the method should * do nothing and return <code>false</code> to signal that the addition has not been successful. * <P> * This method should be implemented by subclasses to filter some nodes, etc. * * @param nodes set of nodes to add to the list * @return <code>true</code> if successfully added */ public abstract boolean add (final Node[] nodes); /** Remove nodes from the list. Only nodes that are present are * removed. * * @param nodes nodes to be removed * @return <code>true</code> if the nodes could be removed */ public abstract boolean remove (final Node[] nodes); /** Get the nodes as an enumeration. * @return enumeration of {@link Node}s */ public final Enumeration nodes () { return new ArrayEnumeration (getNodes ()); } /** Find a child node by name. * This may be overridden in subclasses to provide a more advanced way of finding the * child, but the default implementation simply scans through the list of nodes * to find the first one with the requested name. * <p>Normally the list of nodes should have been computed by the time this returns, * but see {@link #getNodes()} for an important caveat as to why this may not * be doing what you want and what to do instead. * @param name (code) name of child node to find or <code>null</code> if any arbitrary child may * be returned * @return the node or <code>null</code> if it could not be found */ public Node findChild (String name) { Node[] list = getNodes (); if (list.length == 0) { return null; } if (name == null) { // return any node return list[0]; } for (int i = 0; i < list.length; i++) { if (name.equals (list[i].getName ())) { // ok, we have found it return list[i]; } } return null; } /** Method that can be used to test whether the children content has * ever been used or it is still not initalized. * @return true if children has been used before * @see #addNotify */ protected final boolean isInitialized () { ChildrenArray arr = (ChildrenArray)array.get (); return arr != null && arr.isInitialized (); } /** Get a (sorted) array of nodes in this list. * If the children object is not yet initialized, * it will be (using {@link #addNotify}) before * the nodes are returned. * <p><strong>Warning:</strong> not all children * implementations do a complete calculation at * this point, see {@link #getNodes(boolean)} * @return array of nodes */ // private static String off = ""; // NOI18N public final Node[] getNodes () { //Thread.dumpStack(); //System.err.println(off + "getNodes: " + getNode ()); boolean[] results = new boolean[2]; for (;;) { results[1] = isInitialized (); // initializes the ChildrenArray possibly calls // addNotify if this is for the first time ChildrenArray array = getArray(results); // fils results[0] //System.err.println(off + " initialized: " + initialized); // off = off + " "; // NOI18N // forbid any modifications to this hierarchy Node[] nodes; try { PR.enterReadAccess (); nodes = array.nodes (); } finally { PR.exitReadAccess (); } // off = off.substring (2); //System.err.println(off + " length : " + nodes.length); //System.err.println(off + " entries : " + entries); //System.err.println(off + " init now : " + isInitialized()); // if not initialized that means that after // we computed the nodes, somebody changed them (as a // result of addNotify) => we have to compute them // again if (results[1]) { // otherwise it is ok. return nodes; } if (results[0]) { // looks like the result cannot be computed, just give empty one return nodes == null ? new Node[0] : nodes; } } } /** Get a (sorted) array of nodes in this list. * * This method is usefull if you need a fully initialized array of nodes * for things like MenuView, node navigation from scripts/tests and so on. * But in general if you are trying to get useful data by calling * this method, you are probably doing something wrong. * Usually you should be asking some underlying model * for information, not the nodes for children. For example, * {@link org.openide.loaders.DataFolder#getChildren DataFolder.getChildren} * is a much more appropriate way to get what you want. * * If you're extending children, you should make sure this method * will return a complete list of nodes. The default implementation will do * this correctly so long as your subclass implement findChild(null) * to initialize all subnodes. * * <p><strong>Note:</strong>You should not call this method from inside * <code>{@link org.openide.nodes.Children#MUTEX Children.MUTEX}.readAccess()</code>. * If you do so, the <code>Node</code> will be unable to update its state * before you leave the <code>readAccess()</code>. * * @since OpenAPI version 2.17 * * @param optimalResult whether to try to get a fully initialized array * or to simply delegate to {@link #getNodes()} * @return array of nodes */ public Node[] getNodes(boolean optimalResult) { ChildrenArray arr; if (optimalResult) { arr = getArray(null); findChild(null); } return getNodes(); } /** Get the number of nodes in the list. * @return the count */ public final int getNodesCount () { return getNodes ().length; } // // StateNotifications // /** Called when children are first asked for nodes. * Typical implementations at this time calculate * their node list (or keys for {@link Children.Keys} etc.).<BR> * Notice: call to getNodes() inside of this method will return * an empty array of nodes. * @see #isInitialized */ protected void addNotify () { } /** Called when the list of nodes for this children * object is no longer needed by the IDE. * Typical implementations at this time remove all * children to save memory (or clear the keys for * {@link Children.Keys} etc.). */ protected void removeNotify () { } /** Method that can be overriden in subclasses to * do additional work and then call addNotify. */ void callAddNotify () { addNotify (); } // // ChildrenArray operations call only under lock // /** @return either nodes associated with this children or null if * they are not created */ private Node[] testNodes () { ChildrenArray arr = (ChildrenArray)array.get (); return arr == null ? null : arr.nodes (); } private static final Object LOCK = new Object (); /** Obtains references to array holder. If it does not exist, it is * created. * * @param cannotWorkBetter array of size 1 or null, will contain true, if * the getArray cannot be initialized (we are underread access * and nother thread is responbile for initialization, in such case * give up on computation of best result */ private Thread initThread; private ChildrenArray getArray (boolean[] cannotWorkBetter) { ChildrenArray arr; boolean doInitialize = false; synchronized (LOCK) { arr = (ChildrenArray)array.get (); if (arr == null) { arr = new ChildrenArray (); // register the array with the children registerChildrenArray (arr, true); doInitialize = true; initThread = Thread.currentThread(); } } if (doInitialize) { // this call can cause a lot of callbacks => be prepared // to handle them as clean as possible try { this.callAddNotify (); } finally { synchronized (LOCK) { // now attach to children, so when children == null => we are // not fully initialized!!!! arr.children = this; initThread = null; LOCK.notifyAll (); } } } else { // otherwise, if not initialize yet (arr.children) wait // for the initialization to finish, but only if we can wait // we are not in ReadAccess // Children.MUTEX.isReadAccess, if such call would be added to Mutex class MutexChecker implements Runnable { public boolean inReadAccess = true; public void run () { inReadAccess = false; } } MutexChecker test = new MutexChecker(); // the code will run either immediatally or after we leave readAccess // section Children.MUTEX.postWriteRequest(test); if (test.inReadAccess || initThread == Thread.currentThread()) { // fail, we are in read access if (cannotWorkBetter != null) { cannotWorkBetter[0] = true; } return arr; } // otherwise we can wait synchronized (LOCK) { while (arr.children == null) { try { LOCK.wait (); } catch (InterruptedException ex) { } } } } return arr; } /** Clears the nodes */ private void clearNodes () { ChildrenArray arr = (ChildrenArray)array.get (); //System.err.println(off + " clearNodes: " + getNode ()); if (arr != null) { // clear the array arr.clear (); } } /** Forces finalization of nodes for given info. * Called from finalizer of Info. */ final void finalizeNodes () { ChildrenArray arr = (ChildrenArray)array.get (); if (arr != null) { arr.finalizeNodes (); } } /** Registration of ChildrenArray. * @param array use weak or hard references * @param weak use weak or hard reference */ final void registerChildrenArray (final ChildrenArray array, boolean weak) { if (weak) { this.array = new WeakReference (array); } else { // hold the children hard this.array = new WeakReference (array) { public Object get () { return array; } }; } } /** Finalized. */ final void finalizedChildrenArray () { // usually in removeNotify setKeys is called => better require write access try { PR.enterWriteAccess (); if (array.get () == null) { // really finalized and not reconstructed removeNotify (); } } finally { PR.exitWriteAccess (); } } /** Computes the nodes now. */ final Node[] justComputeNodes () { if (map == null) { map = new HashMap (17); // debug.append ("Map initialized\n"); // NOI18N // printStackTrace(); } LinkedList l = new LinkedList (); Iterator it = entries.iterator (); while (it.hasNext ()) { Entry entry = (Entry)it.next (); Info info = findInfo (entry); try { l.addAll (info.nodes ()); } catch (RuntimeException ex) { NodeOp.warning (ex); } } Node[] arr = (Node[])l.toArray (new Node[l.size ()]); // initialize parent nodes for (int i = 0; i < arr.length; i++) { Node n = arr[i]; n.assignTo (this, i); n.fireParentNodeChange (null, parent); } return arr; } /** Finds info for given entry, or registers * it, if not registered yet. */ private Info findInfo (Entry entry) { Info info = (Info)map.get (entry); if (info == null) { info = new Info (entry); map.put (entry, info); // debug.append ("Put: " + entry + " info: " + info); // NOI18N // debug.append ('\n'); // printStackTrace(); } return info; } // // Entries // /** Access to copy of current entries. * @return copy of entries in the objects */ final ArrayList getEntries () { return new ArrayList (this.entries); } final void setEntries (Collection entries) { // current list of nodes ChildrenArray holder = (ChildrenArray)array.get (); if (holder == null) { // debug.append ("Set1: " + entries); // NOI18N // printStackTrace(); this.entries = entries; if (map != null) map.keySet ().retainAll ( new HashSet( entries ) ); return; } Node[] current = holder.nodes (); if (current == null) { // the initialization is not finished yet => // debug.append ("Set2: " + entries); // NOI18N // printStackTrace(); this.entries = entries; if (map != null) map.keySet ().retainAll ( new HashSet( entries ) ); return; } // if there are old items in the map, remove them to // reflect current state map.keySet ().retainAll ( new HashSet( this.entries ) ); // what should be removed HashSet toRemove = new HashSet (map.keySet ()); HashSet entriesSet = new HashSet (entries); toRemove.removeAll (entriesSet); if (!toRemove.isEmpty ()) { // notify removing, the set must be ready for // callbacks with questions updateRemove (current, toRemove); current = holder.nodes (); } // change the order of entries, notifies // it and again brings children to up-to-date state Collection toAdd = updateOrder (current, entries); if (!toAdd.isEmpty ()) { // toAdd contains Info objects that should // be added updateAdd (toAdd, entries); } } /** Removes the objects from the children. */ private void updateRemove (Node[] current, Set toRemove) { LinkedList nodes = new LinkedList (); Iterator it = toRemove.iterator (); while (it.hasNext ()) { Entry en = (Entry)it.next (); Info info = (Info)map.remove (en); //debug.append ("Removed: " + en + " info: " + info); // NOI18N //debug.append ('\n'); //printStackTrace(); nodes.addAll (info.nodes ()); // Has a NullPointerException been thrown? // In such case consider a key implementation. // The key hashCode() and equals() methods // must behave as for an IMMUTABLE object and // the hashCode() must return the same value // for equals() keys. Ccc } // modify the current set of entries and empty the list of nodes // so it has to be recreated again //debug.append ("Current : " + this.entries + '\n'); // NOI18N this.entries.removeAll (toRemove); //debug.append ("Removing: " + toRemove + '\n'); // NOI18N //debug.append ("New : " + this.entries + '\n'); // NOI18N //printStackTrace(); clearNodes (); notifyRemove (nodes, current); } /** Notifies that a set of nodes has been removed from * children. It is necessary that the system is already * in consistent state, so any callbacks will return * valid values. * * @param nodes list of removed nodes * @param current state of nodes * @return array of nodes that were deleted */ Node[] notifyRemove (Collection nodes, Node[] current) { //System.err.println("notifyRemove from: " + getNode ()); //System.err.println("notifyRemove: " + nodes); //System.err.println("Current : " + Arrays.asList (current)); //Thread.dumpStack(); //Keys.last.printStackTrace(); // [TODO] Children do not have always a parent // see Services->FIRST ($SubLevel.class) // during a deserialization it may have parent == null Node[] arr = (Node[])nodes.toArray (new Node[nodes.size ()]); if (parent == null) { return arr; } // fire change of nodes parent.fireSubNodesChange ( false, // remove arr, current ); // fire change of parent Iterator it = nodes.iterator (); while (it.hasNext ()) { Node n = (Node)it.next (); n.deassignFrom (this); n.fireParentNodeChange (parent, null); } return arr; } /** Updates the order of entries. * @param current current state of nodes * @param entries new set of entries * @return list of infos that should be added */ private List updateOrder (Node[] current, Collection entries) { LinkedList toAdd = new LinkedList (); // that assignes entries their begining position in the array // of nodes HashMap offsets = new HashMap (); { int previousPos = 0; Iterator it = this.entries.iterator (); while (it.hasNext ()) { Entry entry = (Entry)it.next (); Info info = (Info)map.get (entry); if (info == null) { throw new IllegalStateException( "Error in " + getClass().getName() + " with entry " + entry + // NOI18N " probably caused by faulty key implementation." + // NOI18N " The key hashCode() and equals() methods must behave as for an IMMUTABLE object" + // NOI18N " and the hashCode() must return the same value for equals() keys."); // NOI18N } offsets.put (info, new Integer (previousPos)); previousPos += info.length (); } } // because map can contain some additional items, // that has not been garbage collected yet, // retain only those that are in current list of // entries map.keySet ().retainAll ( new HashSet( this.entries ) ); int[] perm = new int[current.length]; int currentPos = 0; int permSize = 0; LinkedList reorderedEntries = null; Iterator it = entries.iterator (); while (it.hasNext ()) { Entry entry = (Entry)it.next (); Info info = (Info)map.get (entry); if (info == null) { // this info has to be added info = new Info (entry); toAdd.add (info); } else { int len = info.length (); if (reorderedEntries == null) { reorderedEntries = new LinkedList (); } reorderedEntries.add (entry); // already there => test if it should not be reordered Integer previousInt = (Integer)offsets.get (info); /* if (previousInt == null) { System.err.println("Offsets: " + offsets); System.err.println("Info: " + info); System.err.println("Entry: " + info.entry); System.err.println("This entries: " + this.entries); System.err.println("Entries: " + entries); System.err.println("Map: " + map); System.err.println("---------vvvvv"); System.err.println(debug); System.err.println("---------^^^^^"); } */ int previousPos = previousInt.intValue (); if (currentPos != previousPos) { for (int i = 0; i < len; i++) { perm[previousPos + i] = 1 + currentPos + i; } permSize += len; } } currentPos += info.length (); } if (permSize > 0) { // now the perm array contains numbers 1 to ... and // 0 one places where no permutation occures => // decrease numbers, replace zeros for (int i = 0; i < perm.length; i++) { if (perm[i] == 0) { // fixed point perm[i] = i; } else { // decrease perm[i]--; } } // reorderedEntries are not null this.entries = reorderedEntries; // debug.append ("Set3: " + this.entries); // NOI18N // printStackTrace(); // notify the permutation to the parent clearNodes (); //System.err.println("Paremutaiton! " + getNode ()); Node p = parent; if (p != null) { p.fireReorderChange (perm); } } return toAdd; } /** Updates the state of children by adding given Infos. * @param infos list of Info objects to add * @param entries the final state of entries that should occur */ private void updateAdd (Collection infos, Collection entries) { LinkedList nodes = new LinkedList (); Iterator it = infos.iterator (); while (it.hasNext ()) { Info info = (Info)it.next (); nodes.addAll (info.nodes ()); map.put (info.entry, info); // debug.append ("updateadd: " + info.entry + " info: " + info + '\n'); // NOI18N // printStackTrace(); } this.entries = entries; // debug.append ("Set4: " + entries); // NOI18N // printStackTrace(); clearNodes (); notifyAdd (nodes); } /** Notifies that a set of nodes has been add to * children. It is necessary that the system is already * in consistent state, so any callbacks will return * valid values. * * @param nodes list of removed nodes */ private void notifyAdd (Collection nodes) { // notify about parent change Iterator it = nodes.iterator (); while (it.hasNext ()) { Node n = (Node)it.next (); n.assignTo (this, -1); n.fireParentNodeChange (null, parent); } Node[] arr = (Node[])nodes.toArray (new Node[nodes.size ()]); Node n = parent; if (n != null) { n.fireSubNodesChange ( true, arr, null ); } } /** Refreshes content of one entry. Updates the state of children * appropriatelly. */ final void refreshEntry (Entry entry) { // current list of nodes ChildrenArray holder = (ChildrenArray)array.get (); if (holder == null) { return; } Node[] current = holder.nodes (); if (current == null) { // the initialization is not finished yet => return; } // because map can contain some additional items, // that has not been garbage collected yet, // retain only those that are in current list of // entries map.keySet ().retainAll ( new HashSet( this.entries ) ); Info info = (Info)map.get (entry); if (info == null) { // refresh of entry that is not present => return; } Collection oldNodes = info.nodes (); Collection newNodes = info.entry.nodes (); if (oldNodes.equals (newNodes)) { // nodes are the same => return; } HashSet toRemove = new HashSet (oldNodes); toRemove.removeAll (newNodes); if (!toRemove.isEmpty ()) { // notify removing, the set must be ready for // callbacks with questions // modifies the list associated with the info oldNodes.removeAll (toRemove); clearNodes (); // now everything should be consistent => notify the remove notifyRemove (toRemove, current); current = holder.nodes (); } List toAdd = refreshOrder (entry, oldNodes, newNodes); info.useNodes (newNodes); if (!toAdd.isEmpty ()) { // modifies the list associated with the info clearNodes (); notifyAdd (toAdd); } } /** Updates the order of nodes after a refresh. * @param entry the refreshed entry * @param oldNodes nodes that are currently in the list * @param newNodes new nodes (defining the order of oldNodes and some more) * @return list of infos that should be added */ private List refreshOrder (Entry entry, Collection oldNodes, Collection newNodes) { LinkedList toAdd = new LinkedList (); int currentPos = 0; // cycle thru all entries to find index of the entry Iterator it = this.entries.iterator (); for (;;) { Entry e = (Entry)it.next (); if (e.equals (entry)) { break; } Info info = findInfo (e); currentPos += info.length (); } HashSet oldNodesSet = new HashSet (oldNodes); HashSet toProcess = (HashSet)oldNodesSet.clone (); Node[] permArray = new Node[oldNodes.size ()]; it = newNodes.iterator (); int pos = 0; while (it.hasNext ()) { Node n = (Node)it.next (); if (oldNodesSet.remove (n)) { // the node is in the old set => test for permuation permArray[pos++] = n; } else { if (!toProcess.contains (n)) { // if the node has not been processed yet toAdd.add (n); } else { it.remove (); } } } // JST: If you get IllegalArgumentException in following code // then it can be cause by wrong synchronization between // equals and hashCode methods. First of all check them! int[] perm = NodeOp.computePermutation ( (Node[])oldNodes.toArray (new Node[oldNodes.size ()]), permArray ); if (perm != null) { // apply the permutation clearNodes (); // temporarily change the nodes the entry should use findInfo (entry).useNodes (Arrays.asList (permArray)); Node p = parent; if (p != null) { p.fireReorderChange (perm); } } return toAdd; } /** Information about an entry. Contains number of nodes, * position in the array of nodes, etc. */ final class Info extends Object { int length; Entry entry; public Info (Entry entry) { this.entry = entry; } /** Finalizes the content of ChildrenArray. */ protected void finalize () { finalizeNodes (); } public Collection nodes () { // forces creation of the array ChildrenArray arr = getArray (null); return arr.nodesFor (this); } public void useNodes (Collection nodes) { // forces creation of the array ChildrenArray arr = getArray (null); arr.useNodes (this, nodes); // assign all there nodes the new children Iterator it = nodes.iterator (); while (it.hasNext ()) { Node n = (Node)it.next (); n.assignTo (Children.this, -1); n.fireParentNodeChange (null, parent); } } public int length () { return length; } } /** Interface that provides a set of nodes. */ static interface Entry { /** Set of nodes associated with this entry. * @return list of Node objects */ public Collection nodes (); } /** Empty list of children. Does not allow anybody to insert a node. * Treated especially in the attachTo method. */ private static final class Empty extends Children { Empty() {} /** @return false, does no action */ public boolean add (Node[] nodes) { return false; } /** @return false, does no action */ public boolean remove (Node[] nodes) { return false; } } /** Implements the storage of node children by an array. * Each new child is added at the end of the array. The nodes are * returned in the order they were inserted. * * <p>Normally you will simply create an instance of * <code>Children.Array</code> and add some nodes to it. * If you expect the child nodes to change dynamically, * {@link Children.Keys} is preferable. */ public static class Array extends Children implements Cloneable { /** the entry used for all nodes in the following collection * this object is used for synchronization of operations that * need to be synchronized on this instance of Children, but * we cannot synchronize on this instance because it is public * and somebody else could synchronize too. */ private Entry nodesEntry; /** vector of added children */ protected Collection nodes; /** Constructs a new list and allows a subclass to * provide its own implementation of <code>Collection</code> to store * data in. The collection should be empty and should not * be directly accessed in any way after creation. * * @param c collection to store data in */ protected Array (Collection c) { this (); nodes = c; } /** Constructs a new array children without any assigned collection. * The collection will be created by a call to method initCollection the * first time, children will be used. */ public Array () { nodesEntry = createNodesEntry (); this.setEntries (Collections.singleton (getNodesEntry ())); } /** Clones all nodes that are contained in the children list. * * @return the cloned array for this children */ public Object clone () { try { final Children.Array ar = (Array)super.clone (); try { PR.enterReadAccess (); if (nodes != null) { // nodes already initilized // used to create the right type of collection // clears the content of the collection // JST: hack, but I have no better idea how to write this // pls. notice that in initCollection you can test // whether nodes == null => real initialization // nodes != null => only create new empty collection ar.nodes = ar.initCollection (); ar.nodes.clear (); // insert copies of the nodes Iterator it = nodes.iterator (); while (it.hasNext ()) { Node n = (Node)it.next (); ar.nodes.add (n.cloneNode ()); } } } finally { PR.exitReadAccess (); } return ar; } catch (CloneNotSupportedException e) { // this cannot happen throw new InternalError (); } } /** Allow subclasses to create a collection, the first time the * children are used. It is called only if the collection has not * been passed in the constructor. * <P> * The current implementation returns ArrayList. * * @return empty or initialized collection to use */ protected Collection initCollection () { return new ArrayList (); } /** This method can be called by subclasses that * directly modify the nodes collection to update the * state of the nodes appropriatelly. * This method should be called under * MUTEX.writeAccess. */ final void refreshImpl () { if ( isInitialized() ) { Array.this.refreshEntry (getNodesEntry ()); super.getArray (null).nodes (); } else if ( nodes != null ) { for( Iterator it = nodes.iterator(); it.hasNext(); ) { Node n = (Node)it.next(); n.assignTo( this, -1 ); } } } /** This method can be called by subclasses that * directly modify the nodes collection to update the * state of the nodes appropriatelly. */ protected final void refresh () { MUTEX.postWriteRequest (new Runnable () { public void run () { refreshImpl (); } }); } /** Getter for the entry. */ final Entry getNodesEntry () { return nodesEntry; } /** This method allows subclasses (only in this package) to * provide own version of entry. Usefull for SortedArray. */ Entry createNodesEntry () { return new AE (); } /** Getter for nodes. */ final Collection getCollection () { if (nodes == null) { synchronized (getNodesEntry ()) { if (nodes == null) { nodes = initCollection (); } } } return nodes; } /* * @param arr nodes to add * @return true if changed false if not */ public boolean add (final Node[] arr) { synchronized (getNodesEntry ()) { if (!getCollection ().addAll (Arrays.asList (arr))) { // no change to the collection return false; }; } refresh (); return true; } /* * @param arr nodes to remove * @return true if changed false if not */ public boolean remove (final Node[] arr) { synchronized (getNodesEntry ()) { if (!getCollection ().removeAll (Arrays.asList (arr))) { // the collection was not changed return false; } } refresh (); return true; } /** One entry that holds all the nodes in the collection * member called nodes. */ private final class AE extends Object implements Entry { AE() {} /** List of elements. */ public Collection nodes () { Collection c = getCollection (); if (c.isEmpty ()) { return Collections.EMPTY_LIST; } else { return new ArrayList (c); } } } } /** Implements the storage of node children by a map. * This class also permits * association of a key with any node and to remove nodes by key. * Subclasses should reasonably * implement {@link #add} and {@link #remove}. */ public static class Map extends Children { /** A map to use to store children in. * Keys are <code>Object</code>s, values are {@link Node}s. * Do <em>not</em> modify elements in the map! Use it only for read access. */ protected java.util.Map nodes; /** Constructs a new list with a supplied map object. * Should be used by subclasses desiring an alternate storage method. * The map must not be explicitly modified after creation. * * @param m the map to use for this list */ protected Map (java.util.Map m) { nodes = m; } /** Constructs a new list using {@link HashMap}. */ public Map () { } /** Getter for the map. * Ensures that the map has been initialized. */ final java.util.Map getMap () { // package private only to simplify access from inner classes if (nodes == null) { nodes = initMap (); } return nodes; } /** Called on first use. */ final void callAddNotify () { this.setEntries (createEntries (getMap ())); super.callAddNotify (); } /** Method that allows subclasses (SortedMap) to redefine * order of entries. * @param map the map (Object, Node) * @return collection of (Entry) */ Collection createEntries (java.util.Map map) { LinkedList l = new LinkedList (); Iterator it = map.entrySet ().iterator (); while (it.hasNext ()) { java.util.Map.Entry e = (java.util.Map.Entry)it.next (); l.add (new ME ( e.getKey (), (Node)e.getValue () )); } return l; } /** Allows subclasses that directly modifies the * map with nodes to synchronize the state of the children. * This method should be called under * MUTEX.writeAccess. */ final void refreshImpl () { this.setEntries (createEntries (getMap ())); } /** Allows subclasses that directly modifies the * map with nodes to synchronize the state of the children. */ protected final void refresh () { try { PR.enterWriteAccess (); refreshImpl (); } finally { PR.exitWriteAccess (); } } /** Allows subclasses that directly modifies the * map with nodes to synchronize the state of the children. * This method should be called under * MUTEX.writeAccess. * * @param key the key that should be refreshed */ final void refreshKeyImpl (Object key) { this.refreshEntry (new ME (key, null)); } /** Allows subclasses that directly modifies the * map with nodes to synchronize the state of the children. * * @param key the key that should be refreshed */ protected final void refreshKey (final Object key) { try { PR.enterWriteAccess (); refreshKeyImpl (key); } finally { PR.exitWriteAccess (); } } /** Add a collection of new key/value pairs into the map. * The supplied map may contain any keys, but the values must be {@link Node}s. * * @param map the map with pairs to add */ protected final void putAll (final java.util.Map map) { try { PR.enterWriteAccess (); nodes.putAll (map); refreshImpl (); // PENDING sometime we should also call refreshKey... } finally { PR.exitWriteAccess (); } } /** Add one key and one node to the list. * @param key the key * @param node the node */ protected final void put (final Object key, final Node node) { try { PR.enterWriteAccess (); if (nodes.put (key, node) != null) { refreshKeyImpl (key); } else { refreshImpl (); } } finally { PR.exitWriteAccess (); } } /** Remove some children from the list by key. * @param keys collection of keys to remove */ protected final void removeAll (final Collection keys) { try { PR.enterWriteAccess (); nodes.keySet ().removeAll (keys); refreshImpl (); } finally { PR.exitWriteAccess (); } } /** Remove a given child node from the list by its key. * @param key key to remove */ protected void remove (final Object key) { try { PR.enterWriteAccess (); if (nodes.remove (key) != null) { refreshImpl (); } } finally { PR.exitWriteAccess (); } } /** Initialize some nodes. Allows a subclass to * provide a default map to initialize the map with. * Called only if the map has not been provided in the constructor. * * <P> * The default implementation returns <code>new HashMap (7)</code>. * * @return a map from <code>Object</code>s to {@link Node}s */ protected java.util.Map initMap () { return new HashMap (7); } /** Does nothing. Should be reimplemented in a subclass wishing * to support external addition of nodes. * * @param arr nodes to add * @return <code>false</code> in the default implementation */ public boolean add (Node[] arr) { return false; } /** Does nothing. Should be reimplemented in a subclass wishing * to support external removal of nodes. * @param arr nodes to remove * @return <code>false</code> in the default implementation */ public boolean remove (Node[] arr) { return false; } /** Entry mapping one key to the node. */ final static class ME extends Object implements Entry { /** key */ public Object key; /** node set */ public Node node; /** Constructor. */ public ME (Object key, Node node) { this.key = key; this.node = node; } /** Nodes */ public Collection nodes () { return Collections.singleton (node); } /** Hash code. */ public int hashCode () { return key.hashCode (); } /** Equals. */ public boolean equals (Object o) { if (o instanceof ME) { ME me = (ME)o; return key.equals (me.key); } return false; } public String toString () { return "Key (" + key + ")"; // NOI18N } } } /** Maintains a list of children sorted by the provided comparator in an array. * The comparator can change during the lifetime of the children, in which case * the children are resorted. */ public static class SortedArray extends Children.Array { /** comparator to use */ private Comparator comp; /** Create an empty list of children. */ public SortedArray() { } /** Create an empty list with a specified storage method. * * @param c collection to store data in * @see Children.Array#Children.Array(Collection) */ protected SortedArray (Collection c) { super(c); } /** Set the comparator. The children will be resorted. * The comparator is used to compare Nodes, if no * comparator is used then nodes will be compared by * the use of natural ordering. * * @param c the new comparator */ public void setComparator (final Comparator c) { try { PR.enterWriteAccess (); comp = c; refresh (); } finally { PR.exitWriteAccess (); } } /** Get the current comparator. * @return the comparator */ public Comparator getComparator () { return comp; } /** This method allows subclasses (only in this package) to * provide own version of entry. Usefull for SortedArray. */ Entry createNodesEntry () { return new SAE (); } /** One entry that holds all the nodes in the collection * member called nodes. */ private final class SAE extends Object implements Entry { /** Constructor that provides the original comparator. */ public SAE () { } /** List of elements. */ public Collection nodes () { ArrayList al = new ArrayList (getCollection ()); Collections.sort (al, comp); return al; } } } // end of SortedArray /** Maintains a list of children sorted by the provided comparator in a map. * Similar to {@link Children.SortedArray}. */ public static class SortedMap extends Children.Map { /** comparator to use */ private Comparator comp; /** Create an empty list. */ public SortedMap () { } /** Create an empty list with a specific storage method. * * @param map the map to use with this object * @see Children.Map#Children.Map(java.util.Map) */ protected SortedMap (java.util.Map map) { super(map); } /** Set the comparator. The children will be resorted. * The comparator is used to compare Nodes, if no * comparator is used then values will be compared by * the use of natural ordering. * * @param c the new comparator that should compare nodes */ public void setComparator (final Comparator c) { try { PR.enterWriteAccess (); comp = c; refresh (); } finally { PR.exitWriteAccess (); } } /** Get the current comparator. * @return the comparator */ public Comparator getComparator () { return comp; } /** Method that allows subclasses (SortedMap) to redefine * order of entries. * @param map the map (Object, Node) * @return collection of (Entry) */ Collection createEntries (java.util.Map map) { // SME objects use natural ordering TreeSet l = new TreeSet (new SMComparator ()); Iterator it = map.entrySet ().iterator (); while (it.hasNext ()) { java.util.Map.Entry e = (java.util.Map.Entry)it.next (); l.add (new ME ( e.getKey (), (Node)e.getValue () )); } return l; } /** Sorted map entry can be used for comparing. */ final class SMComparator implements Comparator { public int compare(Object o1, Object o2) { ME me1 = (ME)o1; ME me2 = (ME)o2; Comparator c = comp; if (c == null) { // compare keys return ((Comparable)me1.key).compareTo (me2.key); } else { return c.compare (me1.node, me2.node); } } } } // end of SortedMap /** Implements an array of child nodes associated nonuniquely with keys and sorted by these keys. * There is a {@link #createNodes(Object) method} that should for each * key create an array of nodes that represents the key. * * <p>Typical usage: * <ol> * <li>Subclass. * <li>Decide what type your key should be. * <li>Implement {@link #createNodes} to create some nodes * (usually exactly one) per key. * <li>Override {@link Children#addNotify} to compute a set of keys * and set it using {@link #setKeys(Collection)}. * The collection may be ordered. * <li>Override {@link Children#removeNotify} to just call * <code>setKeys</code> on {@link Collections#EMPTY_SET}. * <li>When your model changes, call <code>setKeys</code> * with the new set of keys. <code>Children.Keys</code> will * be smart and calculate exactly what it needs to do effficiently. * <li><i>(Optional)</i> if your notion of what the node for a * given key changes (but the key stays the same), you can * call {@link #refreshKey}. Usually this is not necessary. * </ol> */ public static abstract class Keys extends Children.Array { /** add array children before or after keys ones */ private boolean before; /** the last runnable (created in method setKeys) for each children object. */ private static HashMap lastRuns = new HashMap (11); /** Special handling for clonning. */ public Object clone () { Keys k = (Keys)super.clone (); return k; } /* Adds additional nodes to the children list. * Works same like Children.Array. * * @param arr nodes to add * @return true */ public boolean add (Node[] arr) { return super.add (arr); } /* Removes nodes added by add from the list. * @param arr nodes to remove * @return if nodes has been removed (they need not necessary be, * because only nodes added by add can be removed, not those * created for key objects) */ public boolean remove (final Node[] arr) { try { PR.enterWriteAccess (); if (nodes != null) { // removing from array, just if the array nodes are // really created // expecting arr.length == 1, which is the usual case for (int i = 0; i < arr.length; i++) { if (!nodes.contains (arr[i])) { arr[i] = null; } } superRemove (arr); } } finally { PR.exitWriteAccess (); } return true; } /** Access method to super impl of remove. */ final void superRemove (Node[] arr) { super.remove (arr); } /** Refresh the child nodes for a given key. * * @param key the key to refresh */ protected final void refreshKey (final Object key) { try { PR.enterWriteAccess (); Keys.this.refreshEntry (new KE (key)); } finally { PR.exitWriteAccess (); } } /** Set new keys for this children object. Setting of keys * does not necessarily lead to the creation of nodes. It happens only * when the list has already been initialized. * * @param keysSet the keys for the nodes (collection of any objects) */ protected final void setKeys (Collection keysSet) { final ArrayList l = new ArrayList (keysSet.size () + 1); if (before) { l.add (getNodesEntry ()); } KE updator = new KE (); updator.updateList (keysSet, l); if (!before) { l.add (getNodesEntry ()); } applyKeys (l); } /** Set keys for this list. * * @param keys the keys for the nodes * @see #setKeys(Collection) */ protected final void setKeys (final Object[] keys) { final ArrayList l = new ArrayList (keys.length + 1); KE updator = new KE (); if (before) { l.add (getNodesEntry ()); } updator.updateList (keys, l); if (!before) { l.add (getNodesEntry ()); } applyKeys (l); } /** Applies the keys. */ private void applyKeys (final ArrayList l) { Runnable invoke = new Runnable () { public void run () { if (keysCheck (Keys.this, this)) { // no next request after me Keys.this.setEntries (l); // clear this runnable keysExit (Keys.this, this); } } }; keysEnter (this, invoke); MUTEX.postWriteRequest (invoke); } /** Set whether new nodes should be added to the beginning or end of sublists for a given key. * * @param b <code>true</code> if the children should be added before */ protected final void setBefore (final boolean b) { try { PR.enterWriteAccess (); if (before != b) { ArrayList l = Keys.this.getEntries (); l.remove (getNodesEntry ()); before = b; if (b) { l.add (0, getNodesEntry ()); } else { l.add (getNodesEntry ()); } Keys.this.setEntries (l); } } finally { PR.exitWriteAccess (); } } /** Create nodes for a given key. * @param key the key * @return child nodes for this key or null if there should be no * nodes for this key */ protected abstract Node[] createNodes (Object key); /** Called when the nodes have been removed from the children. * This method should allow subclasses to clean the nodes somehow. * <p> * Current implementation notifies all listeners on the nodes * that nodes have been deleted. * * @param arr array of deleted nodes */ protected void destroyNodes (Node[] arr) { for (int i = 0; i < arr.length; i++) { arr[i].fireNodeDestroyed (); } } /** Notifies the children class that nodes has been released. */ Node[] notifyRemove (Collection nodes, Node[] current) { Node[] arr = super.notifyRemove (nodes, current); destroyNodes (arr); return arr; } /** Enter of setKeys. * @param ch the children * @param run runnable */ private static synchronized void keysEnter (Keys ch, Runnable run) { lastRuns.put (ch, run); } /** Clears the entry for the children * @param ch children */ private static synchronized void keysExit (Keys ch, Runnable r) { Runnable reg = (Runnable) lastRuns.remove (ch); if (reg != null && !reg.equals (r)) lastRuns.put (ch,reg); } /** Check whether the runnable is "the current" for given children. * @param ch children * @param run runnable * @return true if the runnable shoul run */ private static synchronized boolean keysCheck (Keys ch, Runnable run) { return run == lastRuns.get (ch); } /** Entry for a key */ private final class KE extends Dupl implements Entry { /** Has default constructor that allows to create a factory * of KE objects for use with updateList */ public KE () { } /** Creates directly an instance of the KE object. */ public KE (Object key) { this.key = key; } /** Nodes are taken from the create nodes. */ public Collection nodes () { Node[] arr = createNodes (getKey ()); if (arr == null) { return Collections.EMPTY_LIST; } else { return new LinkedList (Arrays.asList (arr)); } } } } // end of Keys /** Supporting class that provides support for duplicated * objects that still should not be equal. * <P> * It counts the number of times an object has been * added to the collection and if the same object is added * more than once it is indexed by a number. */ private static class Dupl implements Cloneable { /** the key either real value or Dupl (Dupl (Dupl (... value ...)))*/ protected Object key; Dupl() {} /** Updates the second collection with values from the first one. * If there is a multiple occurence of an object in the first collection * a Dupl for the object is created to encapsulate it. */ public final void updateList ( java.util.Collection src, java.util.Collection target ) { java.util.Map map = new java.util.HashMap (src.size () * 2); Iterator it = src.iterator (); while (it.hasNext ()) { Object o = it.next (); updateListAndMap (o, target, map); } } /** Updates the second collection with values from the first array. * If there is a multiple occurence of an object in the first array * a Dupl for the object is created to encapsulate it. */ public final void updateList ( Object[] arr, java.util.Collection target ) { java.util.Map map = new java.util.HashMap (arr.length * 2); for (int i = 0; i < arr.length; i++) { updateListAndMap (arr[i], target, map); } } /** Updates the linked list and the map with right * values. The map is used to count the number of times * a element occures in the list. * * @param obj object to add * @param list the list to add obj to * @param map to track number of occurences in the array (obj, Integer) */ public final void updateListAndMap ( Object obj, java.util.Collection list, java.util.Map map ) { // optimized for first occurence // of each object because often occurences should be rare Object prev = map.put (obj, this); if (prev == null) { // first occurance of object obj list.add (createInstance (obj, 0)); return; } else { if (prev == this) { // second occurence of the object map.put (obj, new Integer (1)); list.add (createInstance (obj, 1)); return; } else { int cnt = ((Integer)prev).intValue () + 1; map.put (obj, new Integer (cnt)); list.add (createInstance (obj, cnt)); return; } } } /** Gets the key represented by this object. * @return the key */ public final Object getKey () { if (key instanceof Dupl) { return ((Dupl)key).getKey (); } else { return key; } } /** Counts the index of this key. * @return integer */ public final int getCnt () { int cnt = 0; Dupl d = this; while (d.key instanceof Dupl) { d = (Dupl)d.key; cnt++; } return cnt; } /** Create instance of Dupl (uses cloning of the class) */ private final Dupl createInstance (Object obj, int cnt) { try { // creates the chain of Dupl (Dupl (Dupl (obj))) where // for cnt = 0 the it would look like Dupl (obj) // for cnt = 1 like Dupl (Dupl (obj)) Dupl d = (Dupl)this.clone (); Dupl first = d; while (cnt-- > 0) { Dupl n = (Dupl)this.clone (); d.key = n; d = n; } d.key = obj; return first; } catch (CloneNotSupportedException ex) { throw new InternalError (); } } public int hashCode () { return getKey ().hashCode (); } public boolean equals (Object o) { if (o instanceof Dupl) { Dupl d = (Dupl)o; return getKey ().equals (d.getKey ()) && getCnt () == d.getCnt (); } return false; } public String toString () { String s = getKey ().toString (); if (s.length () > 80) { s = s.substring (0, 80); } return "Key (" + s + ", " + getCnt () + ")"; // NOI18N } } /* static void printNodes (Node[] n) { for (int i = 0; i < n.length; i++) { System.out.println (" " + i + ". " + n[i].getName () + " number: " + System.identityHashCode (n[i])); } } */ /* JST: Useful test routine ;-) * static { final TopComponent.Registry r = TopComponent.getRegistry (); r.addPropertyChangeListener (new PropertyChangeListener () { Node last = new AbstractNode (LEAF); public void propertyChange (PropertyChangeEvent ev) { Node[] arr = r.getCurrentNodes (); if (arr != null && arr.length == 1) { last = arr[0]; } System.out.println ( "Activated node: " + last + " \nparent: " + last.getParentNode () ); } }); } */ }