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) {
+ }
+ }
+
+}