keys) {
+ setKeys (keys);
+ }
+
+ //We need a type that no Children class will use as a key
+ private static final class WaitNode extends AbstractNode {
+ public WaitNode () {
+ super (Children.LEAF);
+ setDisplayName (NbBundle.getMessage (WaitNode.class, "LBL_WAIT")); //NOI18N
+ }
+
+ public Image getIcon (int type) {
+ return Utilities.loadImage ("org/openide/nodes/wait.gif"); //NOI18N
+ }
+ }
+}
Index: openide/nodes/src/org/openide/nodes/Bundle.properties
===================================================================
RCS file: /cvs/openide/nodes/src/org/openide/nodes/Bundle.properties,v
retrieving revision 1.3
diff -u -r1.3 Bundle.properties
--- openide/nodes/src/org/openide/nodes/Bundle.properties 1 Jul 2006 09:08:57 -0000 1.3
+++ openide/nodes/src/org/openide/nodes/Bundle.properties 21 Mar 2007 04:02:06 -0000
@@ -71,3 +71,5 @@
# NodeMimeTypeCut=application/x-java-openide-nodecut;class=org.openide.nodes.NodeTransfer$Cut
# NodeMimeTypeCopy=application/x-java-openide-nodecopy;class=org.openide.nodes.NodeTransfer$Copy
# NodeMimeTypePaste=application/x-java-openide-nodepaste;class=org.openide.nodes.NodeTransfer$Paste
+#AsynchChildren
+LBL_WAIT=Please Wait...
Index: openide/nodes/src/org/openide/nodes/ChildFactory.java
===================================================================
RCS file: openide/nodes/src/org/openide/nodes/ChildFactory.java
diff -N openide/nodes/src/org/openide/nodes/ChildFactory.java
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ openide/nodes/src/org/openide/nodes/ChildFactory.java 21 Mar 2007 04:02:06 -0000
@@ -0,0 +1,120 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development
+ * and Distribution License (the License). You may not use this file except in
+ * compliance with the License.
+ *
+ * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
+ * or http://www.netbeans.org/cddl.txt.
+ *
+ * When distributing Covered Code, include this CDDL Header Notice in each file
+ * and include the License file at http://www.netbeans.org/cddl.txt.
+ * If applicable, add the following below the CDDL Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyrighted [year] [name of copyright owner]"
+ *
+ * The Original Software is NetBeans. The Initial Developer of the Original
+ * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
+ * Microsystems, Inc. All Rights Reserved.
+ */
+package org.openide.nodes;
+
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
+import java.util.List;
+
+/**
+ * Factory used to create Children
+ * objects. Children objects supply child Nodes for a
+ * Node. Usage is to write a class that extends ChildFactory and
+ * pass that to Children.create(). When the Node is expanded or its
+ * children are programmatically requested, the
+ * createKeys(List <T>) method
+ * will be invoked to create the List of objects to be modelled as Nodes.
+ * Later, on demand, each object from the List will be passed in turn to
+ * createNodes(T) ,
+ * which may return an array of zero or more Nodes for the object.
+ *
+ * A ChildFactory can be used either to create typical Children object, or
+ * one which will be initialized on a background thread (providing
+ * a "Please Wait" Node in the meantime). It can be used most simple cases
+ * that Children.Keys has been historically used for, and makes it easy to
+ * change a Children object to compute its keys asynchronously if that is
+ * needed for performance reasons.
+ *
+ * Only one ChildFactory object may be used per Children object; if you wish
+ * to have multiple Nodes modelling children produced by a single
+ * ChildFactory, use @link FilterNode to wrap the Node that owns the
+ * Children object for this ChildFactory.
+ *
+ * @param T The type of objects in the keys collection
+ * @author Tim Boudreau
+ * @see Children#create(ChildFactory, boolean)
+ */
+public abstract class ChildFactory {
+ /**
+ * Create Nodes for a given key object (one from the List
+ * returned by asynchCreateKeys()).
+ * @param key An object from the list returned by
+ * asynchCreateKeys()
+ * @return zero or more Nodes to represent this key
+ */
+ protected abstract Node[] createNodesForKey (T key);
+ /**
+ * Create a list of keys which can be individually passed to
+ * createNodes() to create child Nodes. Implementations of
+ * this method should regularly check Thread.interrupted(), and
+ * if it returns true (meaning the parent Node was collapsed or
+ * destroyed), stop creating keys immediately and return
+ * true. This method is guaranteed not to be called on the
+ * AWT event thread if this ChildFactory was passed to
+ * Children.create() with the useWaitNode
parameter
+ * set to true. If not, then no guarantees are made as to what
+ * the calling thread is.
+ *
+ * @param toPopulate A list to add key objects to
+ * @return true if the list of keys has been completely populated,
+ * false if the list has only been partially populated and
+ * this method should be called again to batch more keys
+ */
+ protected abstract boolean createKeys (List toPopulate);
+
+ /**
+ * Call this method when the list of objects being modelled by the
+ * has changed and the child Nodes of this Node should be updated. The
+ * boolean argument is a hint to the refresh mechanism (which
+ * will cause createKeys() to be invoked again) that it is safe to
+ * synchronously recreate.
+ *
+ * @param immediate If true, the refresh should occur in the calling
+ * thread (be careful not to be holding any locks that might
+ * deadlock with your key/child creation methods if you pass true).
+ * Note that this parameter is only meaningful when using an
+ * asynchronous children instance (i.e. true was passed as the
+ * second parameter to Children.create()
). If the
+ * Children object for this ChildFactory is called with immediate
+ * true on the AWT event dispatch thread, and it is an asynchronous
+ * Children object, this parameter will be ignored and computation
+ * will be scheduled on a background thread.
+ */
+ protected final void refresh (boolean immediate) {
+ Observer obs = observer == null ? null : observer.get();
+ if (obs != null) {
+ obs.refresh(immediate);
+ }
+ }
+
+ private Reference observer = null;
+ final void setObserver (Observer observer) {
+ if (this.observer != null) {
+ throw new IllegalStateException ("Attempting to create two Children" + //NOI18N
+ " objects for a single ChildFactory. Use " + //NOI18N
+ "FilterNode.Children over the existing Children object " + //NOI18N
+ "instead"); //NOI18N
+ }
+ this.observer = new WeakReference (observer);
+ }
+
+ interface Observer {
+ public void refresh (boolean immediate);
+ }
+}
Index: openide/nodes/src/org/openide/nodes/Children.java
===================================================================
RCS file: /cvs/openide/nodes/src/org/openide/nodes/Children.java,v
retrieving revision 1.20
diff -u -r1.20 Children.java
--- openide/nodes/src/org/openide/nodes/Children.java 14 Feb 2007 15:35:28 -0000 1.20
+++ openide/nodes/src/org/openide/nodes/Children.java 21 Mar 2007 04:02:08 -0000
@@ -39,14 +39,23 @@
import org.openide.util.Enumerations;
import org.openide.util.Mutex;
-/** 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.
+/**
+* Factory for the child Nodes of a Node. Every Node has a Children object.
+* Children are initially un-initialized, and child Nodes are created on
+* demand when, for example, the Node is expanded in an Explorer view.
+* If you know your Node has no child nodes, pass Children.LEAF
.
+* Typically a Children object will create a Collection of objects from
+* some data model, and create one or more Nodes for each object on demand.
+*
+* If initializing the list of children of a Node is time-consuming (i.e. it
+* does I/O, parses a file or some other expensive operation), implement
+* ChildFactory and pass it to Children.create (theFactory, true) to have
+* the child nodes be computed asynchronously on a background thread.
*
-* Probably you want to subclass {@link Children.Keys}.
+*
In almost all cases you want to subclass ChildFactory and pass it to
+* Children.create(), or subclass {@link Children.Keys}.
* Subclassing Children
directly is not recommended.
-*
+*
* @author Jaroslav Tulach
*/
public abstract class Children extends Object {
@@ -219,6 +228,35 @@
PR.exitReadAccess();
}
}
+
+ /**
+ * Create a Children
object using the passed ChildFactory
+ * object. The ChildFactory
will be asked to create a list
+ * of model objects that are the children; then for each object in the list,
+ * ChildFactory.createNodesFor()
will be called to instantiate
+ * one or more Node
s for that object.
+ * @param factory A ChildFactory which will provide child objects
+ * @param useWaitNode If true, the ChildFactory will always be called to
+ * create the list of keys on
+ * a background thread, displaying a "Please Wait" child Node until
+ * some or all child Nodes have been computed. If so,
+ * when it is expanded, the Node that owns
+ * the returned Children
object will display a "please wait"
+ * node while the children are computed in the background. Pass true
+ * for any case where computing child nodes is expensive and should
+ * not be done in the event thread.
+ * @return A Children
object which
+ * will invoke the ChildFactory instance as needed to supply model
+ * objects and child nodes for it
+ * @throws IllegalStateException if the passed ChildFactory has already
+ * been used in a previous call to Children.create()
.
+ * @since 7.1
+ */
+ public static Children create (ChildFactory factory, boolean useWaitNode) {
+ if (factory == null) throw new NullPointerException ("Null factory");
+ return useWaitNode ? new AsynchChildren (factory) :
+ new SynchChildren (factory);
+ }
/** Get the parent node of these children.
* @return the node attached to this children object, or null
if there is none yet
@@ -1913,6 +1951,12 @@
* given key changes (but the key stays the same), you can
* call {@link #refreshKey}. Usually this is not necessary.
*
+ * Note that for simple cases, it may be preferable to subclass
+ * ChildFactory and pass the result to
+ *
+ * create() ; doing so makes it easy to switch to using child
+ * nodes computed on a background thread if necessary for performance
+ * reasons.
* @param T the type of the key
*/
public static abstract class Keys extends Children.Array {
Index: openide/nodes/src/org/openide/nodes/SynchChildren.java
===================================================================
RCS file: openide/nodes/src/org/openide/nodes/SynchChildren.java
diff -N openide/nodes/src/org/openide/nodes/SynchChildren.java
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ openide/nodes/src/org/openide/nodes/SynchChildren.java 21 Mar 2007 04:02:08 -0000
@@ -0,0 +1,63 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development
+ * and Distribution License (the License). You may not use this file except in
+ * compliance with the License.
+ *
+ * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
+ * or http://www.netbeans.org/cddl.txt.
+ *
+ * When distributing Covered Code, include this CDDL Header Notice in each file
+ * and include the License file at http://www.netbeans.org/cddl.txt.
+ * If applicable, add the following below the CDDL Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyrighted [year] [name of copyright owner]"
+ *
+ * The Original Software is NetBeans. The Initial Developer of the Original
+ * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
+ * Microsystems, Inc. All Rights Reserved.
+ */
+package org.openide.nodes;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Synchronous children implementation that takes a ChildFactory.
+ *
+ * @author Tim Boudreau
+ */
+final class SynchChildren extends Children.Keys implements ChildFactory.Observer {
+ private final ChildFactory factory;
+
+ /** Creates a new instance of SynchChildren */
+ SynchChildren(ChildFactory factory) {
+ this.factory = factory;
+ factory.setObserver(this);
+ }
+
+ volatile boolean active = false;
+ protected void addNotify() {
+ active = true;
+ refresh (true);
+ }
+
+ protected void removeNotify() {
+ active = false;
+ setKeys (Collections.emptyList());
+ }
+
+ @SuppressWarnings("unchecked")
+ protected Node[] createNodes(Object key) {
+ return factory.createNodesForKey(key);
+ }
+
+ @SuppressWarnings("unchecked")
+ public void refresh(boolean immediate) {
+ if (active) {
+ List toPopulate = new LinkedList();
+ while (!factory.createKeys(toPopulate)) {}
+ setKeys (toPopulate);
+ }
+ }
+}
Index: openide/nodes/src/org/openide/nodes/wait.gif
===================================================================
RCS file: openide/nodes/src/org/openide/nodes/wait.gif
diff -N openide/nodes/src/org/openide/nodes/wait.gif
Binary files /dev/null and wait.gif differ
Index: openide/nodes/test/unit/src/org/openide/nodes/ChildFactoryTest.java
===================================================================
RCS file: openide/nodes/test/unit/src/org/openide/nodes/ChildFactoryTest.java
diff -N openide/nodes/test/unit/src/org/openide/nodes/ChildFactoryTest.java
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ openide/nodes/test/unit/src/org/openide/nodes/ChildFactoryTest.java 21 Mar 2007 04:02:09 -0000
@@ -0,0 +1,372 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development
+ * and Distribution License (the License). You may not use this file except in
+ * compliance with the License.
+ *
+ * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
+ * or http://www.netbeans.org/cddl.txt.
+ *
+ * When distributing Covered Code, include this CDDL Header Notice in each file
+ * and include the License file at http://www.netbeans.org/cddl.txt.
+ * If applicable, add the following below the CDDL Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyrighted [year] [name of copyright owner]"
+ *
+ * The Original Software is NetBeans. The Initial Developer of the Original
+ * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
+ * Microsystems, Inc. All Rights Reserved.
+ */
+package org.openide.nodes;
+
+import java.awt.EventQueue;
+import java.beans.*;
+import java.util.*;
+import junit.framework.TestCase;
+import org.openide.nodes.*;
+import org.openide.nodes.NodeAdapter;
+import org.openide.util.NbBundle;
+
+/** Test for AsynchChildren
+ *
+ * @author Tim Boudreau
+ */
+public class ChildFactoryTest extends TestCase {
+
+ public ChildFactoryTest(String name) {
+ super(name);
+ }
+
+ private ProviderImpl factory;
+ private BatchProviderImpl factory2;
+ private AsynchChildren kids2;
+ private Node node2;
+ private AsynchChildren kids;
+ private Node node;
+ public void setUp() throws Exception {
+ factory = new ProviderImpl();
+ kids = new AsynchChildren (factory);
+ node = new AbstractNode (kids);
+
+ factory2 = new BatchProviderImpl();
+ kids2 = new AsynchChildren (factory2);
+ node2 = new AbstractNode (kids2);
+ }
+
+ public void testChildrenCreate() {
+ System.out.println("testChildrenCreate");
+ ChildFactory f = new ProviderImpl();
+ Children kids = Children.create (f, true);
+ assertTrue (kids instanceof AsynchChildren);
+
+ ChildFactory ff = new ProviderImpl();
+ Children kids2 = Children.create (ff, false);
+ assertFalse (kids2 instanceof AsynchChildren);
+ assertTrue (kids2 instanceof SynchChildren);
+
+ RuntimeException e = null;
+ Children kids3 = null;
+ try {
+ kids3 = Children.create (ff, true);
+ } catch (RuntimeException ex) {
+ e = ex;
+ }
+ assertNull (kids3);
+ assertNotNull ("Exception should have been thrown creating two " +
+ "Children objects over the same ChildFactory", e);
+ }
+
+ //A word of caution re adding tests:
+ //Almost anything (getNodes(), justCreateNodes(), etc. can trigger a
+ //fresh call to Children.addNotify(). Any test that expects a synchronous
+ //change in the child nodes as a result of having triggered a call
+ //to setKeys() is probably testing a race condition, not the behavior
+ //of the children implementation
+
+ public void testGetNodesWaits() throws Exception {
+ System.out.println("testGetNodesWaits");
+ factory.wait = false;
+ kids.getNodes (false);
+ synchronized (factory.lock) {
+ factory.lock.wait (300);
+ }
+ Thread.currentThread().yield();
+ new NL (node);
+ Node[] n = kids.getNodes(true);
+ assertEquals (4, n.length);
+ }
+
+ public void testInitialNodeIsWaitNode() throws Exception {
+ System.out.println("testInitialNodeIsWaitNode");
+ factory.wait = true;
+ kids.addNotify();
+ Node[] n = kids.getNodes(false);
+ factory.wait = false;
+ assertEquals (1, n.length);
+ assertEquals (NbBundle.getMessage(AsynchChildren.class, "LBL_WAIT"),
+ n[0].getDisplayName());
+ factory.wait = false;
+ synchronized (factory) {
+ factory.wait (2000);
+ }
+ for (int i = 0; i < 5 && n.length != 4; i++) {
+ n = kids.getNodes(true);
+ java.lang.Thread.currentThread().yield();
+ }
+ assertEquals (4, n.length);
+ }
+
+ public void testBatch() throws Exception {
+ System.out.println("testBatch");
+ kids2.addNotify();
+ Thread.currentThread().yield();
+ synchronized (factory2.lock) {
+ factory2.lock.notifyAll();
+ }
+ new NL (node2);
+ Node[] n = n = kids2.getNodes(true);
+ assertEquals (4, n.length);
+ assertEquals (2, factory2.callCount);
+ }
+
+ public void testSynchChildren() throws Exception {
+ System.out.println("testSynchChildren");
+ final SynchProviderImpl factory = new SynchProviderImpl();
+ final Children ch = Children.create (factory, false);
+ assertTrue (ch instanceof SynchChildren);
+ factory.assertCreateKeysNotCalled();
+ factory.assertCreateNodesForKeyNotCalled();
+ final Node nd = new AbstractNode (ch);
+ NodeAdapter adap = new NodeAdapter() {};
+ nd.addNodeListener (adap);
+
+ EventQueue.invokeAndWait (new Runnable() {
+ public void run() {
+ ch.getNodes(true);
+ }
+ });
+ ((SynchChildren) ch).active = true;
+ synchronized (factory) {
+ factory.wait (1000);
+ }
+ factory.assertCreateKeysCalled();
+ factory.assertCreateNodesForKeyCalled();
+ Node[] nodes = nd.getChildren().getNodes(true);
+ assertEquals (factory.CONTENTS1.size(), nodes.length);
+ int ix = 0;
+ for (String s : factory.CONTENTS1) {
+ assertEquals (s, nodes[ix].getName());
+ ix++;
+ }
+ factory.switchChildren();
+ nodes = nd.getChildren().getNodes(true);
+ assertEquals (factory.CONTENTS2.size(), nodes.length);
+ ix = 0;
+ for (String s : factory.CONTENTS2) {
+ assertEquals (s, nodes[ix].getName());
+ ix++;
+ }
+ }
+
+ public void testCancel() throws Exception {
+ System.out.println("testCancel");
+ Thread.interrupted();
+ factory.wait = true;
+ kids.addNotify();
+ Thread.currentThread().yield();
+ synchronized (factory.lock) {
+ factory.lock.wait (500);
+ }
+ kids.removeNotify();
+ factory.wait = false;
+ synchronized (factory) {
+ factory.wait (2000);
+ }
+ assertTrue (kids.cancelled);
+ assertTrue (factory.cancelled);
+ }
+
+ static final class ProviderImpl extends ChildFactory {
+ Object lock = new Object();
+ volatile boolean wait = false;
+
+ public Node[] createNodesForKey(String key) {
+ AbstractNode nd = new AbstractNode (Children.LEAF);
+ nd.setDisplayName (key);
+ return new Node[] { nd };
+ }
+
+ boolean cancelled = false;
+ public boolean createKeys(List result) {
+ try {
+ while (wait) {
+ Thread.currentThread().yield();
+ }
+ synchronized (lock) {
+ lock.notifyAll();
+ }
+ if (Thread.interrupted()) {
+ cancelled = true;
+ return true;
+ }
+ result.add ("A");
+ result.add ("B");
+ result.add ("C");
+ result.add ("D");
+ if (Thread.interrupted()) {
+ cancelled = true;
+ }
+ return true;
+ } finally {
+ synchronized (this) {
+ notifyAll();
+ }
+ }
+ }
+ }
+
+ static final class BatchProviderImpl extends ChildFactory {
+ boolean firstCycle = true;
+
+ public Node[] createNodesForKey(String key) {
+ AbstractNode nd = new AbstractNode (Children.LEAF);
+ nd.setDisplayName (key);
+ return new Node[] { nd };
+ }
+
+ Object lock = new Object();
+ int callCount = 0;
+ public boolean createKeys(List result) {
+ callCount++;
+ synchronized (lock) {
+ try {
+ lock.wait(500);
+ } catch (InterruptedException ex) {
+ //re-interrupt
+ Thread.currentThread().interrupt();
+ }
+ }
+ if (Thread.interrupted()) {
+ return true;
+ }
+ boolean wasFirstCycle = firstCycle;
+ if (wasFirstCycle) {
+ result.add ("A");
+ result.add ("B");
+ firstCycle = false;
+ return false;
+ } else {
+ result.add ("C");
+ result.add ("D");
+ }
+ if (Thread.interrupted()) {
+ return true;
+ }
+ synchronized (this) {
+ notifyAll();
+ }
+ return true;
+ }
+ }
+
+ private static final class NL implements NodeListener {
+ NL (Node n) {
+ n.addNodeListener (this);
+ try {
+ waitFor();
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ } catch (Exception e) {
+ throw new Error (e);
+ }
+ }
+
+ NL () {
+
+ }
+
+ public void childrenAdded(NodeMemberEvent ev) {
+ go();
+ }
+
+ public void childrenRemoved(NodeMemberEvent ev) {
+ go();
+ }
+
+ public void childrenReordered(NodeReorderEvent ev) {
+ go();
+ }
+
+ public void nodeDestroyed(NodeEvent ev) {
+ }
+
+ public void propertyChange(PropertyChangeEvent arg0) {
+ }
+
+ private void go() {
+ synchronized (this) {
+ notifyAll();
+ }
+ }
+
+ void waitFor() throws Exception {
+ System.err.println("Enter waitfor");
+ synchronized (this) {
+ wait (1000);
+ }
+ }
+ }
+
+ private static final class SynchProviderImpl extends ChildFactory {
+ static List CONTENTS1 = Arrays.asList (new String[] {
+ "One", "Two", "Three", "Four"
+ });
+ static List CONTENTS2 = Arrays.asList (new String[] {
+ "Five", "Six", "Seven", "Eight", "Nine"
+ });
+
+ boolean createNodesForKeyCalled = false;
+ public Node[] createNodesForKey(String key) {
+ createNodesForKeyCalled = true;
+ Node result = new AbstractNode(Children.LEAF);
+ result.setDisplayName (key);
+ result.setName (key);
+ return new Node[] { result };
+ }
+
+ boolean createKeysCalled = false;
+ public boolean createKeys(List toPopulate) {
+ createKeysCalled = true;
+ List l = switched ? CONTENTS2 : CONTENTS1;
+ toPopulate.addAll (l);
+ return true;
+ }
+
+ void assertCreateNodesForKeyNotCalled() {
+ assertFalse (createNodesForKeyCalled);
+ }
+
+ void assertCreateKeysNotCalled() {
+ assertFalse (createKeysCalled);
+ }
+
+ boolean assertCreateNodesForKeyCalled() {
+ boolean result = createNodesForKeyCalled;
+ createNodesForKeyCalled = false;
+ assertTrue (result);
+ return result;
+ }
+
+ boolean assertCreateKeysCalled() {
+ boolean result = createKeysCalled;
+ createKeysCalled = false;
+ assertTrue (result);
+ return result;
+ }
+
+ volatile boolean switched = false;
+ void switchChildren() {
+ switched = !switched;
+ refresh(true);
+ }
+ }
+}