diff -r 4a6bce6c911a spi.viewmodel/apichanges.xml --- a/spi.viewmodel/apichanges.xml Tue Sep 22 12:15:56 2009 +0200 +++ b/spi.viewmodel/apichanges.xml Tue Sep 22 17:30:49 2009 +0200 @@ -337,7 +337,27 @@ - + + + Introduction of AsynchronousModel. + + + + + + This API introduced AsynchronousModel and AsynchronousModelFilter that + can be used by clients to define threading of their model implementations. +

+ Added classes:
+ org.netbeans.spi.viewmodel.AsynchronousModel, + org.netbeans.spi.viewmodel.AsynchronousModelFilter, +

+
+ + + +
+ diff -r 4a6bce6c911a spi.viewmodel/manifest.mf --- a/spi.viewmodel/manifest.mf Tue Sep 22 12:15:56 2009 +0200 +++ b/spi.viewmodel/manifest.mf Tue Sep 22 17:30:49 2009 +0200 @@ -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.19 +OpenIDE-Module-Specification-Version: 1.20 diff -r 4a6bce6c911a spi.viewmodel/src/org/netbeans/modules/viewmodel/TreeModelHyperNode.java --- a/spi.viewmodel/src/org/netbeans/modules/viewmodel/TreeModelHyperNode.java Tue Sep 22 12:15:56 2009 +0200 +++ b/spi.viewmodel/src/org/netbeans/modules/viewmodel/TreeModelHyperNode.java Tue Sep 22 17:30:49 2009 +0200 @@ -43,12 +43,16 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Set; + +import org.netbeans.spi.viewmodel.AsynchronousModel.CALL; +import org.netbeans.spi.viewmodel.AsynchronousModel.ThreadProvider; import org.netbeans.spi.viewmodel.Models; import org.netbeans.spi.viewmodel.Models.TreeFeatures; import org.netbeans.spi.viewmodel.TreeModelFilter; import org.netbeans.spi.viewmodel.UnknownTypeException; import org.openide.nodes.Children; import org.openide.nodes.Node; +import org.openide.util.Exceptions; /** * @@ -112,7 +116,31 @@ super(null, model.getColumns(), treeModelRoot, object); this.model = model; } - + + // TODO: Run children of individual models according to individual asynchronous specifications + @Override + protected ThreadProvider getModelAsynchronous() { + ThreadProvider tp = null; + for (Models.CompoundModel m : model.getModels()) { + try { + ThreadProvider t = m.asynchronous(CALL.CHILDREN, object); + if (tp == null) { + tp = t; + } else { + if (!t.isCurrentThread()) { + tp = t; + } + } + } catch (UnknownTypeException ex) { + Exceptions.printStackTrace(Exceptions.attachMessage(ex, "model = "+model+", object = "+object)); + } + } + if (tp == null) { + tp = ThreadProvider.CURRENT_THREAD; + } + return tp; + } + @Override protected Object[] getModelChildren(RefreshingInfo refreshInfo) throws UnknownTypeException { if (refreshInfo instanceof HyperRefreshingInfo) { diff -r 4a6bce6c911a spi.viewmodel/src/org/netbeans/modules/viewmodel/TreeModelNode.java --- a/spi.viewmodel/src/org/netbeans/modules/viewmodel/TreeModelNode.java Tue Sep 22 12:15:56 2009 +0200 +++ b/spi.viewmodel/src/org/netbeans/modules/viewmodel/TreeModelNode.java Tue Sep 22 17:30:49 2009 +0200 @@ -62,6 +62,8 @@ import javax.swing.KeyStroke; import javax.swing.SwingUtilities; +import org.netbeans.spi.viewmodel.AsynchronousModel.CALL; +import org.netbeans.spi.viewmodel.AsynchronousModel.ThreadProvider; import org.netbeans.spi.viewmodel.ColumnModel; import org.netbeans.spi.viewmodel.ModelEvent; import org.netbeans.spi.viewmodel.Models; @@ -106,6 +108,7 @@ private String shortDescription; private final Object shortDescriptionLock = new Object(); private final Map properties = new HashMap(); + private static final String EVALUATING_STR = NbBundle.getMessage(TreeModelNode.class, "EvaluatingProp"); // init .................................................................... @@ -218,6 +221,27 @@ return shortDescription; } } + ThreadProvider tp; + try { + tp = model.asynchronous(CALL.SHORT_DESCRIPTION, object); + } catch (UnknownTypeException ex) { + Exceptions.printStackTrace(Exceptions.attachMessage(ex, "model = "+model+", object = "+object)); + tp = ThreadProvider.CURRENT_THREAD; + } + if (tp.isCurrentThread()) { + return updateShortDescription(); + } else { + tp.getRequestProcessor().post(new Runnable() { + public void run() { + updateShortDescription(); + fireShortDescriptionChange(null, null); + } + }); + return EVALUATING_STR; + } + } + + private String updateShortDescription() { try { String sd = model.getShortDescription (object); if (sd != null) { @@ -404,17 +428,7 @@ boolean refreshed = false; if ((ModelEvent.NodeChanged.DISPLAY_NAME_MASK & changeMask) != 0) { try { - String name = model.getDisplayName (object); - if (name == null) { - Throwable t = - new NullPointerException ( - "Model: " + model + ".getDisplayName (" + object + - ") = null!" - ); - Exceptions.printStackTrace(t); - } else { - setName (name, false); - } + setModelDisplayName(); } catch (UnknownTypeException e) { Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, e); } @@ -459,10 +473,10 @@ private static RequestProcessor requestProcessor; // Accessed from test RequestProcessor getRequestProcessor () { - RequestProcessor rp = treeModelRoot.getRequestProcessor(); + /*RequestProcessor rp = treeModelRoot.getRequestProcessor(); if (rp != null) { return rp; - } + }*/ synchronized (TreeModelNode.class) { if (requestProcessor == null) requestProcessor = new RequestProcessor ("TreeModel", 1); @@ -498,19 +512,59 @@ } } } + + private void setModelDisplayName() throws UnknownTypeException { + ThreadProvider tp; + try { + tp = model.asynchronous(CALL.DISPLAY_NAME, object); + } catch (UnknownTypeException ex) { + Exceptions.printStackTrace(Exceptions.attachMessage(ex, "model = "+model+", object = "+object)); + tp = ThreadProvider.CURRENT_THREAD; + } + if (tp.isCurrentThread()) { + String name = model.getDisplayName (object); + if (name == null) { + Throwable t = + new NullPointerException ( + "Model: " + model + ".getDisplayName (" + object + + ") = null!" + ); + Exceptions.printStackTrace(t); + } else { + setName (name, false); + } + } else { + final String originalDisplayName = getDisplayName(); + setName(EVALUATING_STR, false); + tp.getRequestProcessor().post(new Runnable() { + public void run() { + String name; + try { + name = model.getDisplayName(object); + } catch (UnknownTypeException ex) { + Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, ex); + setName(originalDisplayName, false); + return ; + } + if (name == null) { + Throwable t = + new NullPointerException ( + "Model: " + model + ".getDisplayName (" + object + + ") = null!" + ); + Exceptions.printStackTrace(t); + setName(originalDisplayName, false); + } else { + setName (name, false); + } + } + }); + } + } private void refreshNode () { try { - String name = model.getDisplayName (object); - if (name == null) { - Throwable t = - new NullPointerException ( - "Model: " + model + ".getDisplayName (" + object + - ") = null!" - ); - Exceptions.printStackTrace(t); - } - setName (name, false); + setModelDisplayName(); String iconBase = null; if (model.getRoot() != object) { iconBase = model.getIconBaseWithExtension (object); @@ -834,7 +888,7 @@ /** Special locals subnodes (children) */ static class TreeModelChildren extends Children.Keys - implements LazyEvaluator.Evaluable { + implements Runnable {// LazyEvaluator.Evaluable { private boolean initialezed = false; private final Models.CompoundModel model; @@ -847,6 +901,9 @@ private Object[] children_evaluated; private RefreshingInfo refreshInfo = null; private boolean refreshingStarted = true; + + private RequestProcessor.Task task; + private RequestProcessor lastRp; protected static final Object WAIT_KEY = new Object(); @@ -881,7 +938,7 @@ refreshLazyChildren(refreshSubNodes); } - public void evaluateLazily(Runnable evaluatedNotify) { + public void run() { RefreshingInfo rinfo; synchronized (evaluated) { refreshingStarted = false; @@ -911,7 +968,7 @@ Exceptions.printStackTrace(t); ch = new Object[0]; } - evaluatedNotify.run(); + //evaluatedNotify.run(); boolean fire; synchronized (evaluated) { int eval = evaluated[0]; @@ -942,8 +999,33 @@ count ); } + + protected ThreadProvider getModelAsynchronous() { + ThreadProvider tp; + try { + tp = model.asynchronous(CALL.CHILDREN, object); + } catch (UnknownTypeException ex) { + Exceptions.printStackTrace(Exceptions.attachMessage(ex, "model = "+model+", object = "+object)); + tp = ThreadProvider.CURRENT_THREAD; + } + return tp; + } private void refreshLazyChildren (RefreshingInfo refreshInfo) { + ThreadProvider tp = getModelAsynchronous(); + if (tp.isCurrentThread()) { + Object[] ch; + try { + ch = getModelChildren(refreshInfo); + } catch (UnknownTypeException ex) { + ch = new Object [0]; + if (!(object instanceof String)) { + Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model, ex); + } + } + applyChildren(ch, refreshInfo); + return ; + } synchronized (evaluated) { evaluated[0] = 0; refreshingStarted = true; @@ -954,8 +1036,14 @@ } //System.err.println(this.hashCode()+" refreshLazyChildren() started = true, evaluated = 0"); } + RequestProcessor rp = tp.getRequestProcessor(); + if (rp != lastRp) { + task = rp.create(this); + lastRp = rp; + } + task.schedule(0); // It's refresh => do not check for this children already being evaluated - treeModelRoot.getChildrenEvaluator().evaluate(this, false); + //treeModelRoot.getChildrenEvaluator().evaluate(this, false); Object[] ch; synchronized (evaluated) { if (evaluated[0] != 1) { @@ -1110,14 +1198,16 @@ } } // ItemChildren - private class MyProperty extends PropertySupport implements LazyEvaluator.Evaluable { + private class MyProperty extends PropertySupport implements Runnable { //LazyEvaluator.Evaluable { - private final String EVALUATING_STR = NbBundle.getMessage(TreeModelNode.class, "EvaluatingProp"); private String id; private ColumnModel columnModel; private boolean nodeColumn; private TreeModelRoot treeModelRoot; private final int[] evaluated = { 0 }; // 0 - not yet, 1 - evaluated, -1 - timeouted + + private RequestProcessor.Task task; + private RequestProcessor lastRp; MyProperty ( @@ -1155,7 +1245,7 @@ } } - public void evaluateLazily(Runnable evaluatedNotify) { + public void run() { Object value = ""; String htmlValue = null; String nonHtmlValue = null; @@ -1177,7 +1267,7 @@ } catch (Throwable t) { t.printStackTrace(); } finally { - evaluatedNotify.run(); + //evaluatedNotify.run(); boolean fire; synchronized (properties) { if (value instanceof String) { @@ -1211,10 +1301,28 @@ return properties.get (id); } + ThreadProvider tp; + try { + tp = model.asynchronous(CALL.VALUE, object); + } catch (UnknownTypeException ex) { + Exceptions.printStackTrace(ex); + tp = ThreadProvider.CURRENT_THREAD; + } + + if (tp.isCurrentThread()) { + return getTheValue(); + } + synchronized (evaluated) { evaluated[0] = 0; } - treeModelRoot.getValuesEvaluator().evaluate(this); + RequestProcessor rp = tp.getRequestProcessor(); + if (rp != lastRp) { + task = rp.create(this); + lastRp = rp; + } + task.schedule(0); + //treeModelRoot.getValuesEvaluator().evaluate(this); Object ret = null; @@ -1241,6 +1349,35 @@ // htmlDisplayValue attr will assure that the Evaluating str is there. } return ret; + } + + private Object getTheValue() { + Object value = ""; + String htmlValue = null; + String nonHtmlValue = null; + try { + value = model.getValueAt (object, id); + //System.out.println(" evaluateLazily("+TreeModelNode.this.getDisplayName()+", "+id+"): have value = "+value); + //System.out.println(" object = "+object+" class = "+((object != null) ? object.getClass().toString() : "null")); + if (value instanceof String) { + htmlValue = htmlValue ((String) value); + nonHtmlValue = removeHTML ((String) value); + } + } catch (UnknownTypeException e) { + if (!(object instanceof String)) { + Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Model: "+model+"\n,Column id:" + columnModel.getID (), e); + } + } finally { + synchronized (properties) { + if (value instanceof String) { + properties.put (id, nonHtmlValue); + properties.put (id + "#html", htmlValue); + } else { + properties.put (id, value); + } + } + } + return value; } @Override @@ -1270,20 +1407,48 @@ if (!properties.containsKey(id)) { return null; // The same as value => EVALUATING_STR } + String shortDescription = (String) properties.get (id + "#shortDescription"); + if (shortDescription != null) { + return shortDescription; + } } + ThreadProvider tp; + try { + tp = model.asynchronous(CALL.SHORT_DESCRIPTION, object); + } catch (UnknownTypeException ex) { + Exceptions.printStackTrace(Exceptions.attachMessage(ex, "model = "+model+", value = "+object)); + tp = ThreadProvider.CURRENT_THREAD; + } + if (tp.isCurrentThread()) { + return updateShortDescription(); + } else { + tp.getRequestProcessor().post(new Runnable() { + public void run() { + updateShortDescription(); + firePropertyChange(id, null, null); + } + }); + return null; + } + } + + private String updateShortDescription() { try { javax.swing.JToolTip tooltip = new javax.swing.JToolTip(); + String sd = null; try { tooltip.putClientProperty("getShortDescription", object); // NOI18N Object tooltipObj = model.getValueAt(tooltip, id); - if (tooltipObj == null) { - return null; - } else { - return adjustHTML(tooltipObj.toString()); + if (tooltipObj != null) { + sd = adjustHTML(tooltipObj.toString()); } + return sd; } finally { // We MUST clear the client property, Swing holds this in a static reference! tooltip.putClientProperty("getShortDescription", null); // NOI18N + synchronized (properties) { + properties.put (id + "#shortDescription", sd); + } } } catch (UnknownTypeException e) { // Ignore models that do not define tooltips for values. @@ -1293,30 +1458,41 @@ public void setValue (final Object value) throws IllegalAccessException, IllegalArgumentException, java.lang.reflect.InvocationTargetException { - RequestProcessor prefferedRequestProcessor = treeModelRoot.getRequestProcessor(); - if (prefferedRequestProcessor == null) { - prefferedRequestProcessor = new RequestProcessor("Debugger Values Setter", 1); // NOI18N + ThreadProvider tp; + try { + tp = model.asynchronous(CALL.VALUE, object); + } catch (UnknownTypeException ex) { + Exceptions.printStackTrace(Exceptions.attachMessage(ex, "model = "+model+", object = "+object)); + tp = ThreadProvider.CURRENT_THREAD; } - prefferedRequestProcessor.post(new Runnable() { - public void run() { - try { - Object v = value; - model.setValueAt (object, id, v); - v = model.getValueAt(object, id); // Store the new value - synchronized (properties) { - if (v instanceof String) { - properties.put (id, removeHTML ((String) v)); - properties.put (id + "#html", htmlValue ((String) v)); - } else { - properties.put (id, v); - } - } - firePropertyChange (id, null, null); - } catch (UnknownTypeException e) { - Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Column id:" + columnModel.getID ()+"\nModel: "+model, e); + if (tp.isCurrentThread()) { + setTheValue(value); + } else { + tp.getRequestProcessor().post(new Runnable() { + public void run() { + setTheValue(value); + } + }); + } + } + + private void setTheValue(final Object value) { + try { + Object v = value; + model.setValueAt (object, id, v); + v = model.getValueAt(object, id); // Store the new value + synchronized (properties) { + if (v instanceof String) { + properties.put (id, removeHTML ((String) v)); + properties.put (id + "#html", htmlValue ((String) v)); + } else { + properties.put (id, v); } } - }); + firePropertyChange (id, null, null); + } catch (UnknownTypeException e) { + Logger.getLogger(TreeModelNode.class.getName()).log(Level.CONFIG, "Column id:" + columnModel.getID ()+"\nModel: "+model, e); + } } @Override @@ -1325,10 +1501,10 @@ } } - /** The single-threaded evaluator of lazy models. */ + /** The single-threaded evaluator of lazy models. *//* static class LazyEvaluator implements Runnable { - /** Release the evaluator task after this time. */ + /** Release the evaluator task after this time. *//* private static final long EXPIRE_TIME = 1000L; private final List objectsToEvaluate = new LinkedList(); @@ -1394,7 +1570,7 @@ } - } + }*/ } diff -r 4a6bce6c911a spi.viewmodel/src/org/netbeans/modules/viewmodel/TreeModelRoot.java --- a/spi.viewmodel/src/org/netbeans/modules/viewmodel/TreeModelRoot.java Tue Sep 22 12:15:56 2009 +0200 +++ b/spi.viewmodel/src/org/netbeans/modules/viewmodel/TreeModelRoot.java Tue Sep 22 17:30:49 2009 +0200 @@ -46,6 +46,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.WeakHashMap; import java.util.logging.Level; import java.util.logging.Logger; @@ -89,19 +90,18 @@ private DefaultTreeFeatures treeFeatures; private ExplorerManager manager; - /** The children evaluator for view of this root. */ - private TreeModelNode.LazyEvaluator childrenEvaluator; - /** The values evaluator for view of this root. */ - private TreeModelNode.LazyEvaluator valuesEvaluator; - - /** RequestProcessor to be used for evaluations. */ - private RequestProcessor rp; + /** The children evaluator for view of this root. * + private final Map childrenEvaluators + = new WeakHashMap(); + /** The values evaluator for view of this root. * + private final Map valuesEvaluators + = new WeakHashMap(); + */ public TreeModelRoot (Models.CompoundModel model, TreeView treeView) { this.model = model; this.manager = ExplorerManager.find(treeView); this.treeFeatures = new DefaultTreeFeatures(treeView); - getRP(); modelListeners = new ModelChangeListener[] { new ModelChangeListener(model) }; model.addModelListener (modelListeners[0]); } @@ -111,7 +111,6 @@ this.model = model.getMain(); this.manager = ExplorerManager.find(treeView); this.treeFeatures = new DefaultTreeFeatures(treeView); - getRP(); int nl = model.getModels().length; modelListeners = new ModelChangeListener[nl]; for (int i = 0; i < nl; i++) { @@ -125,7 +124,6 @@ this.model = model; this.manager = ExplorerManager.find(outlineView); this.treeFeatures = new DefaultTreeFeatures(outlineView); - getRP(); modelListeners = new ModelChangeListener[] { new ModelChangeListener(model) }; model.addModelListener (modelListeners[0]); } @@ -135,7 +133,6 @@ this.model = model.getMain(); this.manager = ExplorerManager.find(outlineView); this.treeFeatures = new DefaultTreeFeatures(outlineView); - getRP(); int nl = model.getModels().length; modelListeners = new ModelChangeListener[nl]; for (int i = 0; i < nl; i++) { @@ -145,25 +142,6 @@ } } - private void getRP() { - try { - java.lang.reflect.Field rpf = model.getClass().getDeclaredField("rp"); - rpf.setAccessible(true); - this.rp = (RequestProcessor) rpf.get(model); - } catch (Exception ex) { - Exceptions.printStackTrace(ex); - } - /*if (rp == null) { - Exceptions.printStackTrace(new RuntimeException("NULL RP for "+model)); - } else { - Exceptions.printStackTrace(new RuntimeException("RP for "+model+" is: "+rp)); - }*/ - } - - public RequestProcessor getRequestProcessor() { - return rp; - } - public TreeFeatures getTreeFeatures () { return treeFeatures; } @@ -241,20 +219,26 @@ // } // }); // } - - synchronized TreeModelNode.LazyEvaluator getChildrenEvaluator() { + + /* + synchronized TreeModelNode.LazyEvaluator getChildrenEvaluator(RequestProcessor rp) { + TreeModelNode.LazyEvaluator childrenEvaluator = childrenEvaluators.get(rp); if (childrenEvaluator == null) { childrenEvaluator = new TreeModelNode.LazyEvaluator(rp); + childrenEvaluators.put(rp, childrenEvaluator); } return childrenEvaluator; } - synchronized TreeModelNode.LazyEvaluator getValuesEvaluator() { + synchronized TreeModelNode.LazyEvaluator getValuesEvaluator(RequestProcessor rp) { + TreeModelNode.LazyEvaluator valuesEvaluator = valuesEvaluators.get(rp); if (valuesEvaluator == null) { valuesEvaluator = new TreeModelNode.LazyEvaluator(rp); + valuesEvaluators.put(rp, valuesEvaluator); } return valuesEvaluator; } + */ public synchronized void destroy () { if (model != null) { diff -r 4a6bce6c911a spi.viewmodel/src/org/netbeans/spi/viewmodel/AsynchronousModel.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/spi.viewmodel/src/org/netbeans/spi/viewmodel/AsynchronousModel.java Tue Sep 22 17:30:49 2009 +0200 @@ -0,0 +1,144 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2009 Sun Microsystems, Inc. All rights reserved. + * + * 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 2009 Sun Microsystems, Inc. + */ + +package org.netbeans.spi.viewmodel; + +import org.openide.util.RequestProcessor; + +/** + * Provides threading information about implemented models. + * Methods implemented in {@link TreeModel}, {@link NodeModel} ({@link ExtendedNodeModel}) + * and {@link TableModel} can be called synchronously in AWT thread as a direct + * response to user action, or asynchronously in a Request Processor thread. + * Implementation of this interface can define the threading of method + * calls. + * Register an implementation of this along with other models, + * if you need to change the default threading. + * + * @author Martin Entlicher + * @since 1.20 + */ +public interface AsynchronousModel extends Model { + + /** + * This enumeration identifies method(s) of view models for which + * threading information is provided by {@link #asynchronous(CALL, java.lang.Object)} method. + *
+ * DISPLAY_NAME is for NodeModel.getDisplayName() and ExtendedNodeModel.setName() + *
+ * SHORT_DESCRIPTION for NodeModel.getShortDescription() + *
+ * VALUE for TableModel.getValueAt() and TableModel.setValueAt() + *
+ * CHILDREN for TreeModel.getChildrenCount() and TreeModel.getChildren() + *
+ * The rest of the methods on models are called synchronously, or additional enums can be added. + */ + static enum CALL { DISPLAY_NAME, SHORT_DESCRIPTION, VALUE, CHILDREN } + + /** + * Provide the threading information for view models method calls. + * The returned ThreadProvider gives information about the threading + * of method calls identified by {@link CALL} enum. + * + * @param asynchCall Identification of the method call + * @param node Object node + * @return an instance of ThreadProvider + */ + ThreadProvider asynchronous(CALL asynchCall, Object node) throws UnknownTypeException; + + /** + * Provider of the threading information. + */ + public static final class ThreadProvider extends Object { + + /** + * Threading information for using the current thread for models method calls. + * This will make method invocation synchronous. It's important that the + * methods execute fast so that they do not block AWT thread. + */ + public static final ThreadProvider CURRENT_THREAD = new ThreadProvider(null); + + /** + * Threading information for using a shared {@link RequestProcessor} with + * throughoutput = 1 for models method calls. The UI gives a visual feedback + * to the user if models method calls take a long time. Use this to keep + * the UI responsive. + */ + public static final ThreadProvider DEFAULT_RP = new ThreadProvider(new RequestProcessor("Asynchronous view model", 1)); // NOI18N + + private final RequestProcessor rp; // RP or null for AWT (current thread) + + /** + * Creates a threading information providing the given {@link RequestProcessor} + * for models method calls. Use this to keep the UI responsive when + * models method calls take a long time. + * @param rp the {@link RequestProcessor} to use. + * @return The threading information + */ + public static ThreadProvider createRPProvider(RequestProcessor rp) { + if (rp == null) { + throw new NullPointerException("RequestProcessor must not be null."); // NOI18N + } + return new ThreadProvider(rp); + } + + private ThreadProvider(RequestProcessor rp) { + this.rp = rp; + } + + /** + * Tells if the current thread is to be used for models method calls. + * @return true when the current thread is to be used for + * models method calls. + */ + public boolean isCurrentThread() { + return rp == null; + } + + /** + * Get the {@link RequestProcessor} that is used by this {@link ThreadProvider}, + * if any. + * @return the {@link RequestProcessor} or null. + */ + public RequestProcessor getRequestProcessor() { + return rp; + } + } +} diff -r 4a6bce6c911a spi.viewmodel/src/org/netbeans/spi/viewmodel/AsynchronousModelFilter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/spi.viewmodel/src/org/netbeans/spi/viewmodel/AsynchronousModelFilter.java Tue Sep 22 17:30:49 2009 +0200 @@ -0,0 +1,73 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2009 Sun Microsystems, Inc. All rights reserved. + * + * 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 2009 Sun Microsystems, Inc. + */ + +package org.netbeans.spi.viewmodel; + +import org.netbeans.spi.viewmodel.AsynchronousModel.CALL; +import org.netbeans.spi.viewmodel.AsynchronousModel.ThreadProvider; + +/** + * Change threading information about implemented models. + * Methods implemented in {@link TreeModel}, {@link NodeModel} ({@link ExtendedNodeModel}) + * and {@link TableModel} can be called synchronously in AWT thread as a direct + * response to user action, or asynchronously in a Request Processor thread. + * Implementation of this interface can change the threading of method + * calls provided by an original {@link AsynchronousModel}. + * Register an implementation of this along with other models, + * if you need to change the original threading. + * + * @author Martin Entlicher + * @since 1.20 + */ +public interface AsynchronousModelFilter extends Model { + + /** + * Change the threading information for view models method calls. + * The returned ThreadProvider gives information about the threading + * of method calls identified by {@link CALL} enum. + * + * @param original The original {@link AsynchronousModel} + * @param asynchCall Identification of the method call + * @param node Object node + * @return an instance of ThreadProvider + */ + ThreadProvider asynchronous(AsynchronousModel original, + CALL asynchCall, Object node) throws UnknownTypeException; + +} diff -r 4a6bce6c911a spi.viewmodel/src/org/netbeans/spi/viewmodel/Models.java --- a/spi.viewmodel/src/org/netbeans/spi/viewmodel/Models.java Tue Sep 22 12:15:56 2009 +0200 +++ b/spi.viewmodel/src/org/netbeans/spi/viewmodel/Models.java Tue Sep 22 17:30:49 2009 +0200 @@ -231,23 +231,19 @@ } } List otherModels; - RequestProcessor rp = null; // Either the list contains 10 lists of individual models + one list of mixed models - // + optional TreeExpansionModelFilter(s) + optional RequestProcessor + // + optional TreeExpansionModelFilter(s) + optional AsynchronousModel(s) + optional AsynchronousModelFilter(s) // ; or the models directly boolean hasLists = false; - if (models.size() == 11 || models.size() == 12 || models.size() == 13) { + int modelsSize = models.size(); + if (11 <= modelsSize && modelsSize <= 14) { Iterator it = models.iterator (); boolean failure = false; while (it.hasNext ()) { Object model = it.next(); if (!(model instanceof List)) { - if (model instanceof RequestProcessor && !it.hasNext()) { - failure = false; - } else { - failure = true; - } + failure = true; break; } } @@ -271,20 +267,18 @@ revertOrder(ml.nodeActionsProviderFilters); ml.columnModels = (List) models.get(9); otherModels = (List) models.get(10); - if (models.size() > 11) { // TreeExpansionModelFilter or RequestProcessor - if (models.get(11) instanceof List) { - ml.treeExpansionModelFilters = (List) models.get(11); - } else { - rp = (RequestProcessor) models.get(11); - ml.treeExpansionModelFilters = Collections.emptyList(); + if (modelsSize > 11) { // TreeExpansionModelFilter + ml.treeExpansionModelFilters = (List) models.get(11); + if (modelsSize > 12) { // AsynchronousModel + ml.asynchModels = (List) models.get(12); + if (modelsSize > 13) { // AsynchronousModelFilter + ml.asynchModelFilters = (List) models.get(13); + } } } else { ml.treeExpansionModelFilters = Collections.emptyList(); } //treeExpansionModelFilters = (models.size() > 11) ? (List) models.get(11) : (List) Collections.EMPTY_LIST; - if (models.size() > 12) { - rp = (RequestProcessor) models.get(12); - } } else { // We have the models, need to find out what they implement ml.treeModels = new LinkedList (); ml.treeModelFilters = new LinkedList (); @@ -296,6 +290,8 @@ ml.tableModelFilters = new LinkedList (); ml.nodeActionsProviders = new LinkedList (); ml.nodeActionsProviderFilters = new LinkedList (); + ml.asynchModels = new LinkedList (); + ml.asynchModelFilters = new LinkedList (); ml.columnModels = new LinkedList (); otherModels = (List) models; } @@ -313,10 +309,10 @@ System.out.println("Node Action Provider Filters = "+nodeActionsProviderFilters); System.out.println("Column Models = "+columnModels); */ - return createCompoundModel(ml, propertiesHelpID, rp); + return createCompoundModel(ml, propertiesHelpID); } - private static CompoundModel createCompoundModel (ModelLists ml, String propertiesHelpID, RequestProcessor rp) { + private static CompoundModel createCompoundModel (ModelLists ml, String propertiesHelpID) { if (ml.treeModels.isEmpty ()) { TreeModel etm = new EmptyTreeModel(); ml.treeModels = Collections.singletonList(etm); @@ -333,6 +329,9 @@ defaultExpansionModels.put(ml, defaultExpansionModel); } ml.treeExpansionModels = Collections.singletonList((TreeExpansionModel) defaultExpansionModel); + } + if (ml.asynchModels.isEmpty()) { + ml.asynchModels = Collections.singletonList((AsynchronousModel) new DefaultAsynchronousModel()); } CompoundModel cm = new CompoundModel ( @@ -357,8 +356,11 @@ new DelegatingTableModel (ml.tableModels), ml.tableModelFilters ), - propertiesHelpID, - rp + createCompoundAsynchronousModel ( + new DelegatingAsynchronousModel (ml.asynchModels), + ml.asynchModelFilters + ), + propertiesHelpID ); if (defaultExpansionModel != null) { defaultExpansionModel.setCompoundModel(cm); @@ -523,6 +525,16 @@ expansionModel = new CompoundTreeExpansionModel (expansionModel, filter); } return expansionModel; + } + + private static AsynchronousModel createCompoundAsynchronousModel ( + AsynchronousModel asynchModel, + List filters + ) { + for (AsynchronousModelFilter filter : filters) { + asynchModel = new CompoundAsynchronousModel (asynchModel, filter); + } + return asynchModel; } @@ -1585,6 +1597,21 @@ } + private final static class CompoundAsynchronousModel implements AsynchronousModel { + private AsynchronousModel asynchModel; + private AsynchronousModelFilter asynchModelFilter; + + CompoundAsynchronousModel(AsynchronousModel asynchModel, AsynchronousModelFilter asynchModelFilter) { + this.asynchModel = asynchModel; + this.asynchModelFilter = asynchModelFilter; + } + + public ThreadProvider asynchronous(CALL asynchCall, Object node) throws UnknownTypeException { + return asynchModelFilter.asynchronous(asynchModel, asynchCall, node); + } + + } + /** * Creates one {@link org.netbeans.spi.viewmodel.TableModel} * from given list of TableModels. DelegatingTableModel asks all underlaying @@ -1901,6 +1928,40 @@ return new String (sb); } } + + private static final class DelegatingAsynchronousModel implements AsynchronousModel { + + private AsynchronousModel[] models; + + public DelegatingAsynchronousModel(List models) { + this(models.toArray(new AsynchronousModel[0])); + } + + private DelegatingAsynchronousModel(AsynchronousModel[] models) { + this.models = models; + } + + public ThreadProvider asynchronous(CALL asynchCall, Object node) throws UnknownTypeException { + // It's not much clear how to merge several AsynchronousModels. + // Just let the RP win. + // Usage of AsynchronousModelFilter is preferred. + ThreadProvider tp = null; + for (int i = 0; i < models.length; i++) { + ThreadProvider tpm = models[i].asynchronous(asynchCall, node); + if (tpm != null) { + if (tp == null) { + tp = tpm; + } else { + if (tpm.getRequestProcessor() != null) { + tp = tpm; + } // current otherwise + } + } + } + return tp; + } + + } private static class DefaultTreeExpansionModel implements TreeExpansionModel { @@ -1961,6 +2022,19 @@ return new DefaultTreeExpansionModel(cmRef.get()); } + } + + private static final class DefaultAsynchronousModel implements AsynchronousModel { + + public ThreadProvider asynchronous(CALL asynchCall, Object node) { + if (asynchCall.equals(CALL.CHILDREN) || asynchCall.equals(CALL.VALUE)) { + // For backward compatibility + return ThreadProvider.DEFAULT_RP; + } else { + return ThreadProvider.CURRENT_THREAD; + } + } + } /** @@ -3160,7 +3234,8 @@ * @author Jan Jancura */ public static final class CompoundModel implements TreeModel, - ExtendedNodeModel, CheckNodeModel, NodeActionsProvider, TableModel, TreeExpansionModel { + ExtendedNodeModel, CheckNodeModel, NodeActionsProvider, TableModel, + TreeExpansionModel, AsynchronousModel { private TreeModel treeModel; private ExtendedNodeModel nodeModel; @@ -3169,7 +3244,7 @@ private ColumnModel[] columnModels; private TableModel tableModel; private TreeExpansionModel treeExpansionModel; - private RequestProcessor rp; // Accessed from TreeModelRoot + private AsynchronousModel asynchModel; private CompoundModel mainSubModel; private CompoundModel[] subModels; @@ -3198,8 +3273,8 @@ NodeActionsProvider nodeActionsProvider, List columnModels, TableModel tableModel, - String propertiesHelpID, - RequestProcessor rp + AsynchronousModel asynchModel, + String propertiesHelpID ) { if (treeModel == null) throw new NullPointerException (); if (treeModel == null) throw new NullPointerException (); @@ -3218,8 +3293,8 @@ this.columnModels = columnModels.toArray ( new ColumnModel [columnModels.size ()] ); + this.asynchModel = asynchModel; this.propertiesHelpID = propertiesHelpID; - this.rp = rp; } private CompoundModel(CompoundModel mainSubModel, @@ -3643,6 +3718,12 @@ } } + // AsynchronousModel + + public ThreadProvider asynchronous(CALL asynchCall, Object node) throws UnknownTypeException { + return asynchModel.asynchronous(asynchCall, node); + } + } private static final class ModelLists extends Object { @@ -3658,6 +3739,8 @@ public List nodeActionsProviders = Collections.emptyList(); public List nodeActionsProviderFilters = Collections.emptyList(); public List columnModels = Collections.emptyList(); + public List asynchModels = Collections.emptyList(); + public List asynchModelFilters = Collections.emptyList(); public void addOtherModels(List otherModels) { Iterator it = otherModels.iterator (); @@ -3719,6 +3802,18 @@ else nodeActionsProviderFilters.add(0, (NodeActionsProviderFilter) model); } + if (model instanceof AsynchronousModel) { + asynchModels = new ArrayList(asynchModels); + asynchModels.add((AsynchronousModel) model); + } + if (model instanceof AsynchronousModelFilter) { + asynchModelFilters = new ArrayList(asynchModelFilters); + if (first) + asynchModelFilters.add((AsynchronousModelFilter) model); + else + asynchModelFilters.add(0, (AsynchronousModelFilter) model); + } + if (model instanceof ColumnModel) { columnModels = new ArrayList(columnModels); columnModels.add((ColumnModel) model); diff -r 4a6bce6c911a spi.viewmodel/test/unit/src/org/netbeans/modules/viewmodel/AsynchronousTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/spi.viewmodel/test/unit/src/org/netbeans/modules/viewmodel/AsynchronousTest.java Tue Sep 22 17:30:49 2009 +0200 @@ -0,0 +1,299 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2009 Sun Microsystems, Inc. All rights reserved. + * + * 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 2009 Sun Microsystems, Inc. + */ + +package org.netbeans.modules.viewmodel; + +import java.beans.BeanInfo; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import javax.swing.SwingUtilities; +import org.netbeans.junit.NbTestCase; +import org.netbeans.spi.viewmodel.AsynchronousModel; +import org.netbeans.spi.viewmodel.Model; +import org.netbeans.spi.viewmodel.Models; +import org.netbeans.spi.viewmodel.UnknownTypeException; +import org.openide.nodes.Node; +import org.openide.nodes.Node.Property; +import org.openide.nodes.Node.PropertySet; +import org.openide.util.Exceptions; +import org.openide.util.RequestProcessor; + +/** + * + * @author Martin Entlicher + */ +public class AsynchronousTest extends NbTestCase { + + private static final Set SYNCHRONOUS_METHODS = Collections.unmodifiableSet(new HashSet( + Arrays.asList( "isLeaf", // TreeModel + "getIconBase", "canCopy", "canCut", "canRename", // NodeModel + "clipboardCopy", "clipboardCut", "getPasteTypes", "getIconBaseWithExtension", // ExtendedNodeModel + "isReadOnly" ))); // TableModel + + public AsynchronousTest(String s) { + super(s); + } + + public void testSynchronousAccess() throws Exception { + AWTChecker awtc = new AWTChecker(); + CheckCallingThreadModel cm = new CheckCallingThreadModel(new String[] { "a", "b", "c" }, 2, new ConstantCheckersMap(awtc)); + SynchronousModelImpl sm = new SynchronousModelImpl(); + ArrayList l = new ArrayList (); + l.add(cm); + l.addAll(Arrays.asList(cm.createColumns())); + l.add(sm); + final Models.CompoundModel mcm = Models.createCompoundModel(l); + SwingUtilities.invokeAndWait(new GUIQuerier(mcm)); + Map failedMethods = cm.getFailedMethods(); + assertEquals(failedMethods.toString(), 0, failedMethods.size()); + } + + public void testDefaultRPAccess() throws Exception { + RPChecker rpc = new RPChecker(AsynchronousModel.ThreadProvider.DEFAULT_RP.getRequestProcessor()); + CheckCallingThreadModel cm = new CheckCallingThreadModel(new String[] { "a", "b", "c" }, 2, new ConstantCheckersMap(rpc)); + DefaultRPModelImpl drpm = new DefaultRPModelImpl(); + ArrayList l = new ArrayList (); + l.add(cm); + l.addAll(Arrays.asList(cm.createColumns())); + l.add(drpm); + final Models.CompoundModel mcm = Models.createCompoundModel(l); + SwingUtilities.invokeAndWait(new GUIQuerier(mcm)); + Map failedMethods = cm.getFailedMethods(); + assertEquals(failedMethods.toString(), 0, failedMethods.size()); + } + + private static final class GUIQuerier implements Runnable { + + private final Models.CompoundModel mcm; + + public GUIQuerier(Models.CompoundModel mcm) { + this.mcm = mcm; + } + + public void run() { + OutlineTable tt = (OutlineTable) Models.createView(mcm); + Node root = tt.getExplorerManager ().getRootContext (); + + root.getChildren().getNodes(); + root.getHtmlDisplayName(); + root.getShortDescription(); + for (Node n : root.getChildren().getNodes()) { + inspectNode(n); + for (Node nn : n.getChildren().getNodes()) { + inspectNode(nn); + } + } + } + + private void inspectNode(Node n) { + n.getDisplayName(); + n.getHtmlDisplayName(); + n.getShortDescription(); + n.getIcon(BeanInfo.ICON_COLOR_16x16); + n.canCopy(); + n.canCut(); + n.canRename(); + n.getNewTypes(); + n.getActions(true); + n.getPreferredAction(); + inspectProperties(n); + } + + private void inspectProperties(Node n) { + PropertySet[] propertySets = n.getPropertySets(); + for (PropertySet ps : propertySets) { + for (Property p : ps.getProperties()) { + try { + p.getValue(); + } catch (IllegalAccessException ex) { + Exceptions.printStackTrace(ex); + } catch (InvocationTargetException ex) { + Exceptions.printStackTrace(ex); + } + p.canRead(); + p.canWrite(); + p.getName(); + p.getDisplayName(); + p.getHtmlDisplayName(); + p.getShortDescription(); + } + } + } + } + + private static interface ThreadChecker { + + boolean isInCorrectThread(); + + } + + private static final class AWTChecker implements ThreadChecker { + + public boolean isInCorrectThread() { + return SwingUtilities.isEventDispatchThread(); + } + + } + + private static final class RPChecker implements ThreadChecker { + + private RequestProcessor rp; + + public RPChecker(RequestProcessor rp) { + this.rp = rp; + } + + public boolean isInCorrectThread() { + return rp.isRequestProcessorThread(); + } + + } + + private final class ConstantCheckersMap implements Map { + + private final ThreadChecker tc; + + public ConstantCheckersMap(ThreadChecker tc) { + this.tc = tc; + } + + public int size() { return Integer.MAX_VALUE; } + + public boolean isEmpty() { return false; } + + public boolean containsKey(Object key) { return true; } + + public boolean containsValue(Object value) { return value == tc; } + + public Object get(Object key) { + if (SYNCHRONOUS_METHODS.contains(key)) { + return new AWTChecker(); + } + return tc; + } + + public Object put(Object key, Object value) { throw new UnsupportedOperationException("N/A"); } + + public Object remove(Object key) { throw new UnsupportedOperationException("N/A"); } + + public void putAll(Map t) { throw new UnsupportedOperationException("N/A"); } + + public void clear() { throw new UnsupportedOperationException("N/A"); } + + public Set keySet() { throw new UnsupportedOperationException("N/A"); } + + public Collection values() { return Collections.singleton(tc); } + + public Set entrySet() { throw new UnsupportedOperationException("N/A"); } + + } + + private static class CheckCallingThreadModel extends CountedModel { + + private final Map threadCheckers; + private final Map failedMethods = new HashMap(); + + public CheckCallingThreadModel(String[] children, int depth, + Map threadCheckers) { + super(children, depth); + this.threadCheckers = threadCheckers; + } + + @Override + protected void countCall(String methodName, Object... params) { + if ("getValueAt".equals(methodName) && params[0] instanceof javax.swing.JToolTip) { + methodName = "getShortDescription (called on property values)"; + } + ThreadChecker tc = threadCheckers.get(methodName); + if (!tc.isInCorrectThread()) { + failedMethods.put(methodName, Thread.currentThread()); + //Thread.dumpStack(); + } + super.countCall(methodName, params); + } + + Map getFailedMethods() { + return failedMethods; + } + + } + + private static class SynchronousModelImpl implements AsynchronousModel { + + public ThreadProvider asynchronous(CALL asynchCall, Object node) throws UnknownTypeException { + return ThreadProvider.CURRENT_THREAD; + } + + } + + private static class DefaultRPModelImpl implements AsynchronousModel { + + public ThreadProvider asynchronous(CALL asynchCall, Object node) throws UnknownTypeException { + return ThreadProvider.DEFAULT_RP; + } + + } + + private static class CustomRPModelImpl implements AsynchronousModel { + + private Map rps; + + public CustomRPModelImpl(Map rps) { + this.rps = rps; + } + + public ThreadProvider asynchronous(CALL asynchCall, Object node) throws UnknownTypeException { + RequestProcessor rp = rps.get(asynchCall); + if (rp != null) { + return ThreadProvider.createRPProvider(rp); + } else { + return ThreadProvider.CURRENT_THREAD; + } + } + + } + +} diff -r 4a6bce6c911a spi.viewmodel/test/unit/src/org/netbeans/modules/viewmodel/CountedModel.java --- a/spi.viewmodel/test/unit/src/org/netbeans/modules/viewmodel/CountedModel.java Tue Sep 22 12:15:56 2009 +0200 +++ b/spi.viewmodel/test/unit/src/org/netbeans/modules/viewmodel/CountedModel.java Tue Sep 22 17:30:49 2009 +0200 @@ -263,7 +263,7 @@ } } - private void countCall(String methodName, Object... params) { + protected void countCall(String methodName, Object... params) { CountedCall cc = new CountedCall(methodName, params); if (!countedCalls.add(cc)) { for (CountedCall ecc : countedCalls) {