Index: openide/nodes/src/org/openide/nodes/AsynchChildren.java =================================================================== RCS file: openide/nodes/src/org/openide/nodes/AsynchChildren.java diff -N openide/nodes/src/org/openide/nodes/AsynchChildren.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openide/nodes/src/org/openide/nodes/AsynchChildren.java 24 Dec 2006 21:58:54 -0000 @@ -0,0 +1,181 @@ +/* + * 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.awt.Image; +import java.lang.Thread; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import org.openide.util.NbBundle; +import org.openide.util.RequestProcessor; +import org.openide.util.Utilities; + +/** + * Children object which creates its keys on a background thread. To use, + * implement AsynchChildren.Provider and pass that to the constructor. + * + * @author Tim Boudreau + */ +public final class AsynchChildren extends Children.Keys { + private final Provider provider; + private final RequestProcessor.Task task; + private final RequestProcessor PROC = new RequestProcessor("Asynch " //NOI18N + + "children creator " + this, Thread.NORM_PRIORITY, true); //NOI18N + private final R r = new R(); + /** + * Create a new AsyncChildren instance with the passed provider object + * which will manufacture key objects and nodes. + * @param provider An object which can provide a list of keys and make + * Nodes for them + */ + public AsynchChildren(Provider provider) { + this.provider = provider; + task = PROC.create(r, true); + } + + volatile boolean initialized = false; + protected void addNotify() { + if ((!initialized && task.isFinished()) || cancelled) { + cancelled = false; + setKeys (new Object[] { new WaitNode() }); + task.schedule(0); + Thread.dumpStack(); + } + } + + protected void removeNotify() { + cancelled = true; + task.cancel(); + initialized = false; + setKeys (Collections.emptyList()); + } + + /** + * Notify this AsynchChildren that it should reconstruct its children, + * calling provider.asynchCreateKeys() and setting the + * keys to that. Call this method if the list of child objects is known + * or likely to have changed. + * @param immediate If true, the keys are updated synchronously from the + * calling thread. Set this to true only if you know that updating + * the keys will not be an expensive or time-consuming operation. + */ + @SuppressWarnings("unchecked") //NOI18N + public void update(boolean immediate) { + if (immediate) { + boolean done; + List keys = new LinkedList (); + do { + done = provider.asynchCreateKeys(keys); + } while (!done); + setKeys (keys); + } else { + task.schedule (0); + } + } + + public Node[] getNodes(boolean optimalResult) { + if (optimalResult) { + task.waitFinished(); + } + return super.getNodes(); + } + + @SuppressWarnings("unchecked") + protected Node[] createNodes(Object key) { + if (key instanceof WaitNode) { + return new Node[] { (Node) key }; + } else { + return provider.createNodes (key); + } + } + + /** + * Factory for keys and child nodes for an instance of AsynchChildren. + */ + public static interface Provider { + /** + * 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 + */ + public Node[] createNodes (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 + * null. This method is guaranteed not to be called on the + * AWT event thread. + * + * @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 + */ + public boolean asynchCreateKeys (List toPopulate); + } + + volatile boolean cancelled = false; + private final class R implements Runnable { + @SuppressWarnings("unchecked") + public void run() { + if (Thread.interrupted()) { + System.err.println("Cancelled"); + doSetKeys (Collections.EMPTY_LIST); + return; + } + List keys = new LinkedList (); + boolean done; + int ct = 0; + do { + System.err.println("LOOP " + (ct++)); + done = provider.asynchCreateKeys (keys); + if (cancelled || Thread.interrupted()) { + doSetKeys (Collections.emptyList()); + System.err.println("Cancelled"); + return; + } + doSetKeys (new ArrayList (keys)); + System.err.println("Batch set " + keys + " DONE " + done); + } while (!done); + initialized = done; + } + + private void doSetKeys (List 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 24 Dec 2006 21:58:54 -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 + +LBL_WAIT=Please Wait... 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/AsynchChildrenTest.java =================================================================== RCS file: openide/nodes/test/unit/src/org/openide/nodes/AsynchChildrenTest.java diff -N openide/nodes/test/unit/src/org/openide/nodes/AsynchChildrenTest.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openide/nodes/test/unit/src/org/openide/nodes/AsynchChildrenTest.java 24 Dec 2006 21:58:54 -0000 @@ -0,0 +1,251 @@ +/* + * 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.beans.*; +import java.util.*; +import junit.framework.TestCase; +import org.openide.nodes.*; +import org.openide.util.NbBundle; + +/** Test for AsynchChildren + * + * @author Tim Boudreau + */ +public class AsynchChildrenTest extends TestCase { + + public AsynchChildrenTest(String name) { + super(name); + } + + private ProviderImpl provider; + private BatchProviderImpl provider2; + private AsynchChildren kids2; + private Node node2; + private AsynchChildren kids; + private Node node; + public void setUp() throws Exception { + provider = new ProviderImpl(); + kids = new AsynchChildren (provider); + node = new AbstractNode (kids); + + provider2 = new BatchProviderImpl(); + kids2 = new AsynchChildren (provider2); + node2 = new AbstractNode (kids2); + } + + //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"); + kids.addNotify(); + new NL (node); + Node[] n = kids.getNodes(true); + assertEquals (4, n.length); + } + + public void testInitialNodeIsWaitNode() throws Exception { + System.out.println("testInitialNodeIsWaitNode"); + provider.wait = true; + kids.addNotify(); + Node[] n = kids.justComputeNodes(); + assertEquals (1, n.length); + assertEquals (NbBundle.getMessage(AsynchChildren.class, "LBL_WAIT"), + n[0].getDisplayName()); + provider.wait = false; + synchronized (provider) { + provider.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 testCancel() throws Exception { + System.out.println("testCancel"); + provider.wait = true; + kids.addNotify(); + kids.removeNotify(); + provider.wait = false; + synchronized (provider) { + provider.wait (2000); + } + assertTrue (provider.cancelled); + } + + public void testBatch() throws Exception { + System.out.println("testBatch"); + provider2.wait = true; + kids2.addNotify(); + provider2.wait = false; + new NL (node2); + Node[] n = n = kids2.getNodes(true); + assertEquals (4, n.length); + assertEquals (2, provider2.callCount); + } + + static final class ProviderImpl implements AsynchChildren.Provider { + + boolean wait = false; + + public Node[] createNodes(String key) { + AbstractNode nd = new AbstractNode (Children.LEAF); + nd.setDisplayName (key); + return new Node[] { nd }; + } + + boolean cancelled = false; + public boolean asynchCreateKeys(List result) { + try { + while (wait) { + try { + Thread.currentThread().sleep (200); + } catch (Exception e) { + + } + } + 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; + } + return true; + } finally { + synchronized (this) { + notifyAll(); + } + } + } + } + + static final class BatchProviderImpl implements AsynchChildren.Provider { + boolean firstCycle = true; + volatile boolean wait = false; + + public Node[] createNodes(String key) { + AbstractNode nd = new AbstractNode (Children.LEAF); + nd.setDisplayName (key); + return new Node[] { nd }; + } + + private synchronized void waitFor() { + try { + wait(5000); + } catch (InterruptedException ex) { + throw new Error (ex); + } + } + + int callCount = 0; + public boolean asynchCreateKeys(List result) { + callCount++; + int wc = 0; + System.err.println("Enter wait"); + while (wait && (wc ++ < 20)) { + try { + Thread.currentThread().sleep (200); + } catch (Exception e) { + + } + } + System.err.println("Exit wait"); + if (Thread.interrupted()) { + return true; + } + boolean wasFirstCycle = firstCycle; + if (wasFirstCycle) { + result.add ("A"); + result.add ("B"); + wait = true; + 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 (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 { + synchronized (this) { + wait (10000); + } + } + } +}