diff -r 549fcf920254 spi.viewmodel/apichanges.xml --- a/spi.viewmodel/apichanges.xml Fri Nov 14 20:54:49 2014 +0100 +++ b/spi.viewmodel/apichanges.xml Thu Nov 20 14:58:37 2014 +0100 @@ -432,6 +432,22 @@ + + + Add an abstract children caching model. + + + + + + Views that show asynchronously loaded children nodes, have problems + with visible refreshes of the children tree. + CachedChildrenTreeModel is introduced to allow seamless + refresh of children tree. + + + + diff -r 549fcf920254 spi.viewmodel/manifest.mf --- a/spi.viewmodel/manifest.mf Fri Nov 14 20:54:49 2014 +0100 +++ b/spi.viewmodel/manifest.mf Thu Nov 20 14:58:37 2014 +0100 @@ -1,5 +1,5 @@ Manifest-Version: 1.0 OpenIDE-Module: org.netbeans.spi.viewmodel/2 OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/viewmodel/Bundle.properties -OpenIDE-Module-Specification-Version: 1.47 +OpenIDE-Module-Specification-Version: 1.48 diff -r 549fcf920254 spi.viewmodel/src/org/netbeans/spi/viewmodel/CachedChildrenTreeModel.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/spi.viewmodel/src/org/netbeans/spi/viewmodel/CachedChildrenTreeModel.java Thu Nov 20 14:58:37 2014 +0100 @@ -0,0 +1,232 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2010 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, 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-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2008 Sun Microsystems, Inc. + */ + +package org.netbeans.spi.viewmodel; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.Executor; +import org.netbeans.spi.viewmodel.AsynchronousModelFilter.CALL; + +/** + * A TreeModel, which caches children objects and allow seamless update of children objects. + * + * @author Martin Entlicher + * @since 1.48 + */ +public abstract class CachedChildrenTreeModel extends Object implements TreeModel, AsynchronousModelFilter { + + private final Map childrenCache = new WeakHashMap(); + private final Set childrenToRefresh = new HashSet(); + + @Override + public Executor asynchronous(Executor original, CALL asynchCall, Object node) throws UnknownTypeException { + if (CALL.CHILDREN.equals(asynchCall)) { + boolean cache = cacheChildrenOf(node); + if (cache) { + synchronized (childrenCache) { + if (childrenToRefresh.remove(node)) { + childrenCache.remove(node); + return original; + } + if (childrenCache.containsKey(node)) { + return AsynchronousModelFilter.CURRENT_THREAD; + } + } + } + } + return original; + } + + @Override + public final Object[] getChildren (Object o, int from, int to) + throws UnknownTypeException { + Object[] ch; + boolean cache = cacheChildrenOf(o); + if (cache) { + ChildrenTree cht; + synchronized (childrenCache) { + //ch = (List) childrenCache.get(o); + cht = childrenCache.get(o); + } + if (cht != null) { + ch = cht.getChildren(); + } else { + ch = null; + } + } else ch = null; + if (ch == null) { + ch = computeChildren(o); + if (ch == null) { + throw new UnknownTypeException (o); + } else { + if (cache) { + ChildrenTree cht = new ChildrenTree(o); + cht.setChildren(ch); + synchronized (childrenCache) { + childrenCache.put(o, cht); + } + } + } + } + ch = reorder(ch); + int l = ch.length; + from = Math.min(l, from); + to = Math.min(l, to); + if (from == 0 && to == l) { + return ch; + } else { + Object[] ch1 = new Object[to - from]; + System.arraycopy(ch, from, ch1, 0, to - from); + ch = ch1; + } + return ch; + } + + /** + * Compute the children nodes. This is called when there are no children + * cached for this node only. + * @param node The node to compute the children for + * @return The list of children + * @throws UnknownTypeException When this implementation is not able to + * resolve children for given node type + */ + protected abstract Object[] computeChildren(Object node) throws UnknownTypeException; + + /** + * Can be overridden to decide which nodes to cache and which not. + * @param node The node + * @return true when the children of this node should be cached, + * false otherwise. The default implementation returns + * true always. + */ + protected boolean cacheChildrenOf(Object node) { + return true; + } + + /** + * Force a refresh of the cache. + * @param node The node to refresh the cache for. + */ + protected final void refreshCache(Object node) { + synchronized (childrenCache) { + childrenToRefresh.add(node); + } + } + + /** + * Clear the entire cache. + */ + protected final void clearCache() { + synchronized (childrenCache) { + childrenCache.clear(); + childrenToRefresh.clear(); + } + } + + /** + * Allows to reorder the children. This is called each time the children + * are requested, even when they're cached. + * @param nodes The original nodes returned by {@link #computeChildren(java.lang.Object)} + * or by the cache. + * @return The reordered nodes. The default implementation returns the original nodes. + */ + protected Object[] reorder(Object[] nodes) { + return nodes; + } + + /** + * Force to recompute all cached children. + * @throws UnknownTypeException When this implementation is not able to + * resolve children for some node type + */ + protected final void recomputeChildren() throws UnknownTypeException { + recomputeChildren(getRoot()); + } + + /** + * Force to recompute children cached for the given node. + * @param node The node to recompute the children for + * @throws UnknownTypeException When this implementation is not able to + * resolve children for the given node type + */ + protected final void recomputeChildren(Object node) throws UnknownTypeException { + ChildrenTree cht; + Set keys; + synchronized (childrenCache) { + cht = childrenCache.get(node); + keys = childrenCache.keySet(); + } + if (cht != null) { + Object[] oldCh = cht.getChildren(); + Object[] newCh = computeChildren(node); + cht.setChildren(newCh); + for (int i = 0; i < newCh.length; i++) { + if (keys.contains(newCh[i])) { + recomputeChildren(newCh[i]); + } + } + } + } + + private final static class ChildrenTree { + + //private Object node; + private Object[] ch; + + public ChildrenTree(Object node) { + //this.node = node; + } + + public void setChildren(Object[] ch) { + this.ch = ch; + } + + public Object[] getChildren() { + return ch; + } + + } + +} diff -r 549fcf920254 spi.viewmodel/test/unit/src/org/netbeans/modules/viewmodel/CachedChildrenTreeModelTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/spi.viewmodel/test/unit/src/org/netbeans/modules/viewmodel/CachedChildrenTreeModelTest.java Thu Nov 20 14:58:37 2014 +0100 @@ -0,0 +1,347 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2014 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, 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-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2014 Sun Microsystems, Inc. + */ + +package org.netbeans.modules.viewmodel; + +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.netbeans.spi.viewmodel.AsynchronousModelFilter; +import org.netbeans.spi.viewmodel.CachedChildrenTreeModel; +import org.netbeans.spi.viewmodel.UnknownTypeException; +import static org.junit.Assert.*; +import org.netbeans.spi.viewmodel.ModelListener; +import org.netbeans.spi.viewmodel.TreeModel; + +/** + * + * @author martin + */ +public class CachedChildrenTreeModelTest { + + public CachedChildrenTreeModelTest() { + } + + @BeforeClass + public static void setUpClass() { + } + + @AfterClass + public static void tearDownClass() { + } + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + /** + * Test of getChildren method, of class CachedChildrenTreeModel. + */ + @Test + public void testGetChildren() throws Exception { + System.out.println("getChildren"); + int from = 0; + int to = Integer.MAX_VALUE; + CachedChildrenTreeModel instance = new CachedChildrenTreeModelImpl(); + Object[] result = instance.getChildren("", from, to); + assertArrayEquals(new Object[] {"a", "b", "c"}, result); + result = instance.getChildren("b", from, to); + assertArrayEquals(new Object[] {"ba", "bb", "bc"}, result); + } + + /** + * Test of computeChildren method, of class CachedChildrenTreeModel. + */ + @Test + public void testComputeChildren() throws Exception { + System.out.println("computeChildren"); + int from = 0; + int to = Integer.MAX_VALUE; + CachedChildrenTreeModelImpl instance = new CachedChildrenTreeModelImpl(); + assertFalse(instance.isChildernComputed()); + Object[] result = instance.getChildren("", from, to); + assertTrue(instance.isChildernComputed()); + assertArrayEquals(new Object[] {"a", "b", "c"}, result); + // Ask again + result = instance.getChildren("", from, to); + assertFalse(instance.isChildernComputed()); + + result = instance.getChildren("b", from, to); + assertTrue(instance.isChildernComputed()); + assertArrayEquals(new Object[] {"ba", "bb", "bc"}, result); + + result = instance.getChildren("b", from, to); + assertFalse(instance.isChildernComputed()); + result = instance.getChildren("", from, to); + assertFalse(instance.isChildernComputed()); + assertArrayEquals(new Object[] {"a", "b", "c"}, result); + } + + /** + * Test of cacheChildrenOf method, of class CachedChildrenTreeModel. + */ + @Test + public void testCacheChildrenOf() throws Exception { + System.out.println("cacheChildrenOf"); + int from = 0; + int to = Integer.MAX_VALUE; + CachedChildrenTreeModelImpl instance = new CachedChildrenTreeModelImpl(); + assertFalse(instance.isChildernComputed()); + Object[] result = instance.getChildren("", from, to); + assertTrue(instance.isChildernComputed()); + assertArrayEquals(new Object[] {"a", "b", "c"}, result); + result = instance.getChildren("c", from, to); + assertTrue(instance.isChildernComputed()); + assertArrayEquals(new Object[] {"ca", "cb", "cc"}, result); + result = instance.getChildren("cc", from, to); + assertTrue(instance.isChildernComputed()); + assertArrayEquals(new Object[] {"cca", "ccb", "ccc"}, result); + result = instance.getChildren("cc", from, to); + // cc is always re-computed + assertTrue(instance.isChildernComputed()); + result = instance.getChildren("c", from, to); + assertFalse(instance.isChildernComputed()); + } + + /** + * Test of refreshCache method, of class CachedChildrenTreeModel. + */ + @Test + public void testRefreshCache() throws Exception { + System.out.println("refreshCache"); + int from = 0; + int to = Integer.MAX_VALUE; + CachedChildrenTreeModelImpl instance = new CachedChildrenTreeModelImpl(); + assertFalse(instance.isChildernComputed()); + Object[] result = instance.getChildren("", from, to); + assertTrue(instance.isChildernComputed()); + assertArrayEquals(new Object[] {"a", "b", "c"}, result); + result = instance.getChildren("a", from, to); + assertTrue(instance.isChildernComputed()); + assertArrayEquals(new Object[] {"aa", "ab", "ac"}, result); + + result = instance.getChildren("", from, to); + result = instance.getChildren("a", from, to); + assertFalse(instance.isChildernComputed()); + + instance.doRefreshCache("a"); + instance.asynchronous(null, AsynchronousModelFilter.CALL.CHILDREN, "a"); + result = instance.getChildren("a", from, to); + assertTrue(instance.isChildernComputed()); + } + + /** + * Test of clearCache method, of class CachedChildrenTreeModel. + */ + @Test + public void testClearCache() throws Exception { + System.out.println("clearCache"); + int from = 0; + int to = Integer.MAX_VALUE; + CachedChildrenTreeModelImpl instance = new CachedChildrenTreeModelImpl(); + assertFalse(instance.isChildernComputed()); + Object[] result = instance.getChildren("", from, to); + assertTrue(instance.isChildernComputed()); + assertArrayEquals(new Object[] {"a", "b", "c"}, result); + result = instance.getChildren("a", from, to); + assertTrue(instance.isChildernComputed()); + assertArrayEquals(new Object[] {"aa", "ab", "ac"}, result); + + result = instance.getChildren("", from, to); + result = instance.getChildren("a", from, to); + assertFalse(instance.isChildernComputed()); + + instance.doClearCache(); + result = instance.getChildren("", from, to); + assertTrue(instance.isChildernComputed()); + result = instance.getChildren("a", from, to); + assertTrue(instance.isChildernComputed()); + } + + /** + * Test of reorder method, of class CachedChildrenTreeModel. + */ + @Test + public void testReorder() throws Exception { + System.out.println("reorder"); + int from = 0; + int to = Integer.MAX_VALUE; + CachedChildrenTreeModelImpl instance = new CachedChildrenTreeModelImpl() { + @Override + protected Object[] reorder(Object[] nodes) { + return new Object[] { nodes[2], nodes[0], nodes[1] }; + } + }; + Object[] result = instance.getChildren("", from, to); + assertArrayEquals(new Object[] {"c", "a", "b"}, result); + } + + /** + * Test of recomputeChildren method, of class CachedChildrenTreeModel. + */ + @Test + public void testRecomputeChildren_0args() throws Exception { + System.out.println("recomputeChildren"); + int from = 0; + int to = Integer.MAX_VALUE; + CachedChildrenTreeModelImpl instance = new CachedChildrenTreeModelImpl(); + assertFalse(instance.isChildernComputed()); + Object[] result = instance.getChildren("", from, to); + assertTrue(instance.isChildernComputed()); + assertArrayEquals(new Object[] {"a", "b", "c"}, result); + result = instance.getChildren("a", from, to); + assertTrue(instance.isChildernComputed()); + assertArrayEquals(new Object[] {"aa", "ab", "ac"}, result); + + result = instance.getChildren("", from, to); + result = instance.getChildren("a", from, to); + assertFalse(instance.isChildernComputed()); + + instance.doRecomputeChildren(); + assertTrue(instance.isChildernComputed()); + + result = instance.getChildren("", from, to); + result = instance.getChildren("a", from, to); + assertFalse(instance.isChildernComputed()); + } + + /** + * Test of recomputeChildren method, of class CachedChildrenTreeModel. + */ + @Test + public void testRecomputeChildren_Object() throws Exception { + System.out.println("recomputeChildren"); + int from = 0; + int to = Integer.MAX_VALUE; + CachedChildrenTreeModelImpl instance = new CachedChildrenTreeModelImpl(); + assertFalse(instance.isChildernComputed()); + Object[] result = instance.getChildren("", from, to); + assertTrue(instance.isChildernComputed()); + assertArrayEquals(new Object[] {"a", "b", "c"}, result); + result = instance.getChildren("a", from, to); + assertTrue(instance.isChildernComputed()); + assertArrayEquals(new Object[] {"aa", "ab", "ac"}, result); + + result = instance.getChildren("", from, to); + result = instance.getChildren("a", from, to); + assertFalse(instance.isChildernComputed()); + + instance.doRecomputeChildren("a"); + assertTrue(instance.isChildernComputed()); + + result = instance.getChildren("", from, to); + result = instance.getChildren("a", from, to); + assertFalse(instance.isChildernComputed()); + } + + public class CachedChildrenTreeModelImpl extends CachedChildrenTreeModel { + + private AtomicBoolean childernComputed = new AtomicBoolean(false); + + @Override + public Object[] computeChildren(Object node) throws UnknownTypeException { + String s = (String) node; + childernComputed.set(true); + return new Object[] { s+"a", s+"b", s+"c" }; + } + + boolean isChildernComputed() { + return childernComputed.getAndSet(false); + } + + @Override + protected boolean cacheChildrenOf(Object node) { + if ("cc".equals(node)) { + return false; + } + return super.cacheChildrenOf(node); + } + + void doRefreshCache(Object node) { + refreshCache(node); + } + + void doClearCache() { + clearCache(); + } + + void doRecomputeChildren() throws UnknownTypeException { + recomputeChildren(); + } + + void doRecomputeChildren(Object node) throws UnknownTypeException { + recomputeChildren(node); + } + + @Override + public Object getRoot() { + return ""; + } + + @Override + public boolean isLeaf(Object node) throws UnknownTypeException { + return false; + } + + @Override + public int getChildrenCount(Object node) throws UnknownTypeException { + return Integer.MAX_VALUE; + } + + @Override + public void addModelListener(ModelListener l) { + } + + @Override + public void removeModelListener(ModelListener l) { + } + } + +}