Index: openide/src/org/openide/explorer/view/LazyListModel.java =================================================================== RCS file: openide/src/org/openide/explorer/view/LazyListModel.java diff -N openide/src/org/openide/explorer/view/LazyListModel.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openide/src/org/openide/explorer/view/LazyListModel.java 9 Aug 2004 16:21:20 -0000 1.1.2.11 @@ -0,0 +1,353 @@ +/* + * Sun Public License Notice + * + * The contents of this file are subject to the Sun Public License + * Version 1.0 (the "License"). You may not use this file except in + * compliance with the License. A copy of the License is available at + * http://www.sun.com/ + * + * The Original Code is NetBeans. The Initial Developer of the Original + * Code is Sun Microsystems, Inc. Portions Copyright 1997-2000 Sun + * Microsystems, Inc. All Rights Reserved. + */ + +package org.openide.explorer.view; + +import java.util.BitSet; +import javax.swing.*; +import javax.swing.event.*; + +/** Model that can compute its values lazily and moreover handle some + * kind of filtering. +*/ +final class LazyListModel extends Object implements ListModel, Runnable { + private boolean log; + private ListModel listModel; + private Filter filter; + private double ratio; + /** the value to return when nothing else can be returned */ + private Object defaultValue; + /** simple event listener list */ + private javax.swing.event.EventListenerList list = new javax.swing.event.EventListenerList (); + + /** the size we currently pretend to have */ + private int size; + /** contains 1 if the bit mask if the index in listModel has already been tested */ + private BitSet tested; + /** this index is returned as our index number X */ + private int[] mapping; + /** contains 1 if the this bit has been excluded (of course it would have to be tested first) */ + private BitSet excluded; + /** original state of bits in excluded at the time we did last notification */ + private BitSet original; + /** set with indexes that have been removed (and they could influence our knowledge */ + private BitSet noticeableRemovals; + /** set with Integer indexes that we lies about providing default value */ + private java.util.SortedSet defaultLies; + + private LazyListModel (ListModel m, Filter f, double expectedRadio, Object defaultValue) { + this.listModel = m; + this.filter = f; + this.ratio = expectedRadio; + this.defaultValue = defaultValue; + } + + final Filter getFilter () { + return filter; + } + + final boolean isComputed (int index) { + return tested != null && tested.get (index); + } + + /** When executed, updateYourAssumeptions. + */ + public void run () { + if (log) { + System.err.println("model.updateYourAssumeptions ();"); // NOI18N + } + updateYourAssumeptions (); + } + + /** Can be called to ask the LazyListModel to update its assumptions, + * especially assumptions about the size to match its current knowledge. + */ + final void updateYourAssumeptions () { + if (defaultLies == null && noticeableRemovals == null) return; + + int notifiedRemovals = 0; + for (int index = noticeableRemovals.nextSetBit (0); index >= 0; ) { + int end = noticeableRemovals.nextClearBit (index); + if (end == -1) { + // till the end + end = originalSize (); + } + + // we have influenced something behind us, let's notify + int cnt = end - index; + int myIndex = myIndexOf (index, excluded); + int myEnd = myIndex + cnt; + notifiedRemovals += cnt; + + // update mapping + for (int i = 0; i < mapping.length; i++) { + if (mapping[i] >= myEnd) { + mapping[i] -= cnt; + } + } + + // update the original truth and fire! + ListDataEvent ev = new ListDataEvent ( + this, ListDataEvent.INTERVAL_REMOVED, myIndex, myEnd + ); + fireChange (ev); + + // remove all the deleted indexes from defaultLies + if (defaultLies != null) { + java.util.Set s = defaultLies.subSet (new Integer (myIndex), new Integer (myEnd)); + s.clear (); + } + + // continue from the end + index = noticeableRemovals.nextSetBit (end); + } + + int currentSize = originalSize () - excluded.cardinality (); + if (notifiedRemovals + currentSize < size) { + // notify removal at the end of the list + int endIndex = size - notifiedRemovals; + // update the original truth and fire! + ListDataEvent ev = new ListDataEvent ( + this, ListDataEvent.INTERVAL_REMOVED, currentSize, endIndex + ); + fireChange (ev); + } + size = currentSize; + + /* + if (defaultLies != null && !defaultLies.isEmpty ()) { + java.util.Iterator it = defaultLies.iterator (); + while (it.hasNext ()) { + Integer faked = (Integer)it.next (); + + ListDataEvent ev = new ListDataEvent ( + this, ListDataEvent.CONTENTS_CHANGED, faked.intValue (), faked.intValue () + 1 + ); + + } + } + */ + + // clean up as we fired everything + defaultLies = null; + noticeableRemovals = null; + original = excluded; + } + + /** Factory method to create new filtering lazy model. + */ + public static LazyListModel create (ListModel listModel, Filter f, double expectedRadio, Object defValue) { + LazyListModel lazy = new LazyListModel (listModel, f, expectedRadio, defValue); + return lazy; + } + + /** Model with enabled logging. + */ + static LazyListModel create (ListModel listModel, Filter f, double expectedRadio, Object defValue, boolean log) { + LazyListModel lazy = new LazyListModel (listModel, f, expectedRadio, defValue); + lazy.log = log; + return lazy; + } + + // + // Model methods. + // + + public void addListDataListener(ListDataListener l) { + list.add (ListDataListener.class, l); + } + + public void removeListDataListener(ListDataListener l) { + list.remove (ListDataListener.class, l); + } + + private void fireChange (ListDataEvent ev) { + if (list.getListenerCount () == 0) return ; + + Object[] arr = list.getListenerList (); + for (int i = arr.length - 1; i >= 0; i -= 2) { + ListDataListener l = (ListDataListener)arr[i]; + switch (ev.getType ()) { + case ListDataEvent.CONTENTS_CHANGED: l.contentsChanged (ev); break; + case ListDataEvent.INTERVAL_ADDED: l.intervalAdded (ev); break; + case ListDataEvent.INTERVAL_REMOVED: l.intervalRemoved (ev); break; + default: + throw new IllegalArgumentException ("Unknown type: " + ev.getType ()); + } + } + } + + /** Initialize the bitsets to sizes of the listModel. + */ + private void initialize () { + if (tested == null) { + size = listModel.getSize (); + tested = new BitSet (size); + excluded = new BitSet (size); + original = excluded; + mapping = new int[size]; + } + } + + /** In a given BitSet finds my index of given index. + */ + private static int myIndexOf (int index, BitSet set) { + // this is number of unexcluded objects before the index + return index - set.get (0, index).cardinality (); + } + + /** Checks whether the object on index in the listModel is acceptable + * for the filter. + * @param index the index to check + * @return true or false + */ + private boolean acceptable (int index, int forWhatIndexWeAreAsking) { + // here use the excluded version (more uptodate) than original as + // we are interested only about the current knowledge of the + // filtered elements and we want to prevent asking twice about the + // same index + if (index < 0) return false; + if (index >= originalSize ()) return false; + if (excluded.get (index)) return false; + // if not excluded and already tested, we know it is ok + if (tested.get (index)) { + return mapping[index] == forWhatIndexWeAreAsking; + } + + // otherwise do the test yourself + tested.set (index); + mapping[index] = forWhatIndexWeAreAsking; + + boolean ok = filter.accept (listModel.getElementAt (index)); + if (!ok) { + if (noticeableRemovals == null) { + // this notifies the updateYourAssumeptions method that it needs to run + noticeableRemovals = new BitSet (originalSize ()); + } + BitSet theFollowers = tested.get (index + 1, originalSize ()); + if (!theFollowers.isEmpty ()) { + // this index removal needs to be notified + noticeableRemovals.set (index); + } + if (excluded == original) { + excluded = (BitSet)original.clone (); + } + excluded.set (index); + filter.scheduleUpdate (this); + } + return ok; + } + + /** Size of the original model. + */ + private int originalSize () { + return listModel.getSize (); + } + + /** In case that our index if not acceptable, we try to grasp another index around. + * It has to be unexaminated, otherwise we return false and of course has to be + * acceptable. + * + * @param index index to test + * @param increasing are we searching up or down? + * @param result assign the resulting object to index 0 of the array + * @return false we reached an already examinated index + */ + private boolean fallbackIndex (int index, boolean increasing, Object[] result, int forWhatIndexWeAreAsking) { + if (index < 0) return false; + if (index >= originalSize ()) return false; + + if (acceptable (index, forWhatIndexWeAreAsking)) { + result[0] = listModel.getElementAt (index); + return true; + } + + if (increasing) { + // if the i-th index is a + return mapping[index] <= forWhatIndexWeAreAsking; + } else { + return mapping[index] >= forWhatIndexWeAreAsking; + } + } + + public Object getElementAt(int index) { + initialize (); + + if (log) { + System.err.println("model.getElementAt (" + index + ");"); // NOI18N + } + + /* JST: Write a test for this code + if (defaultLies != null && defaultLies.contains (new Integer (index))) { + return defaultValue; + } + */ + + /** JST: Or this needs to use always the original BitSet, otherwise we + * will get different results in two subsequent calls + */ + + // here we have to use original instead of excluded because we want this + // information to be always immutable between notifications of listeners + int skip = 0; + for (int i = original.nextSetBit (0); i <= index + skip && i >= 0; i = original.nextSetBit (i + 1)) { + // we found an excluded bit + skip++; + } + + // now index + skip is the index in the original array to ask for + assert index + skip < originalSize () : "Never can go beyond the size of the array: size = " + originalSize () + " index: " + index + " skip: " + skip + " excluded: " + excluded; // NOI18N + assert !original.get (index + skip) : "This bit must always be non-excluded, at least in the original version before notification"; // NOI18N + + int myIndex = index + skip; + if (acceptable (myIndex, index)) { + return listModel.getElementAt (myIndex); + } + + boolean checkBefore = true; + boolean checkAfter = true; + Object[] result = new Object[1]; + for (int i = 1; checkAfter || checkBefore; i++) { + if (checkBefore) checkBefore = fallbackIndex (myIndex - i, false, result, index); + if (result[0] != null) return result[0]; + if (checkAfter) checkAfter = fallbackIndex (myIndex + i, true, result, index); + if (result[0] != null) return result[0]; + } + + // no acceptable element found, try default value + if (defaultLies == null) { + defaultLies = new java.util.TreeSet (); + } + defaultLies.add (new Integer (index)); + return defaultValue; + } + + public int getSize() { + initialize (); + return size; + } + + + /** Interface for those that wish to filter content of the list. + * This filter is expected to always return the same result for + * the same object - e.g. either always exclude or include it. + */ + interface Filter { + public boolean accept (Object obj); + /** This method is called when the list needs update. It's goal is + * usually to do SwingUtilities.invokeLater, even more rafined + * methods are allowed. + */ + public void scheduleUpdate (Runnable run); + } +} Index: openide/test/unit/src/org/openide/explorer/view/LazyListModelTest.java =================================================================== RCS file: openide/test/unit/src/org/openide/explorer/view/LazyListModelTest.java diff -N openide/test/unit/src/org/openide/explorer/view/LazyListModelTest.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openide/test/unit/src/org/openide/explorer/view/LazyListModelTest.java 9 Aug 2004 16:21:32 -0000 1.1.2.11 @@ -0,0 +1,420 @@ +/* + * Sun Public License Notice + * + * The contents of this file are subject to the Sun Public License + * Version 1.0 (the "License"). You may not use this file except in + * compliance with the License. A copy of the License is available at + * http://www.sun.com/ + * + * The Original Code is NetBeans. The Initial Developer of the Original + * Code is Sun Microsystems, Inc. Portions Copyright 1997-2004 Sun + * Microsystems, Inc. All Rights Reserved. + */ + +/* + * + */ +package org.openide.explorer.view; + +import javax.swing.event.ListDataEvent; +import junit.textui.TestRunner; + +import org.netbeans.junit.NbTestCase; +import org.netbeans.junit.NbTestSuite; + +/** Framework for testing the behaviour of lazy filtering lists. + * @author Jaroslav Tulach + */ +public class LazyListModelTest extends NbTestCase implements javax.swing.event.ListDataListener { + private java.util.LinkedList events = new java.util.LinkedList (); + + public LazyListModelTest (String name) { + super(name); + } + + public static void main (String args[]) { + if (args.length == 1) { + TestRunner.run (new LazyListModelTest (args[0])); + } else { + TestRunner.run (new NbTestSuite (LazyListModelTest.class)); + } + } + + public void testAllAreAccepted () { + Model m = new Model (10); + LazyListModel.Filter f = new LazyListModel.Filter () { + public boolean accept (Object o) { + return true; + } + public void scheduleUpdate (Runnable r) { + } + }; + + LazyListModel lazy = LazyListModel.create (m, f, 1.0f, null); + + m.assertModel ("They should be the same", lazy); + assertEquals ("Size is supposed to be 10", 10, lazy.getSize ()); + + for (int i = 0; i < 10; i++) { + assertEquals (i + "th element", new Integer (i), lazy.getElementAt (i)); + } + assertEquals ("Size stays on 10", 10, lazy.getSize ()); + m.assertModel ("Models remain the same", lazy); + } + + + public void testExpectedRatioIsHigher () { + doAcceptHalf (false, 1.0, 5); + + assertNextEvent ("There is five missing elements, so we should get a message", ListDataEvent.INTERVAL_REMOVED, 5, 10); + assertNoEvents (); + } + + public void testOddAreAccepted () { + doAcceptHalf (false, 0.5, 5); + } + + public void testEvenAreAccepted () { + LazyListModel model = doAcceptHalf (true, 0.5, 6); + + assertNextEvent ("Removal of 6 to 10", ListDataEvent.INTERVAL_REMOVED, 6, 10); + assertNoEvents (); + + assertEquals ("Ok, the model thinks it has six elements" + + " but it does not, it will need to return default value as its sixth" + + " value", null, model.getElementAt (5)); + + model.updateYourAssumeptions (); + + assertEquals ("And its size has to grow down", 5, model.getSize ()); + + assertNextEvent ("One removal notified", ListDataEvent.INTERVAL_REMOVED, 5, 6); + } + + private LazyListModel doAcceptHalf (final boolean even, double ratio, int expectedSize) { + // our current implemntation works only for ratio 1 + ratio = 1.0; + + + + Model m = new Model (10); + EvenOrOdd f = new EvenOrOdd (even); + + LazyListModel lazy = LazyListModel.create (m, f, ratio, null); + lazy.addListDataListener (this); + + m.assertModel ("They should be the same", lazy); + assertEquals ("Size is supposed to be 5", (int)(10 * ratio), lazy.getSize ()); + + int add = even ? 0 : 1; + for (int i = 0; i < 5; i++) { + Object element = lazy.getElementAt (i); + assertEquals (i + "th element", new Integer (i * 2 + add), element); + } + + lazy.updateYourAssumeptions (); + + assertEquals ("Size is ok", expectedSize, lazy.getSize ()); + m.assertModel ("Models remain the same", lazy); + + return lazy; + } + + // next round of tests + + public void testAcceptJustFirstAndLast () { + Model m = new Model (10); + java.util.HashSet h = new java.util.HashSet (); + h.add (m.getElementAt (0)); + h.add (m.getElementAt (9)); + + Object defaultValue = new Object(); + LazyListModel lazy = LazyListModel.create (m, new SetFilter (h), 1.0, defaultValue); + lazy.addListDataListener (this); + + assertEquals ("Due to ratio 1.0 the expected size is 10", 10, lazy.getSize ()); + assertEquals (new Integer (0), lazy.getElementAt (0)); + assertEquals (new Integer (9), lazy.getElementAt (9)); + + Object value = lazy.getElementAt (1); + assertEquals ("There is nothing to return, so just return default value", defaultValue, value); + lazy.updateYourAssumeptions (); + assertEquals ("Size has to drop to 2", 2, lazy.getSize ()); + assertNextEvent ("Middle of interval is gone", ListDataEvent.INTERVAL_REMOVED, 1, 9); + assertNoEvents (); + + // what is the value of value? + + assertEquals ("second element is now nine", new Integer (9), lazy.getElementAt (1)); + } + + public void testTheAnswersAreImmutableUntilUpdateYourAssumptionsIsCalled () { + Model m = new Model (10); + java.util.HashSet h = new java.util.HashSet (); + h.add (m.getElementAt (0)); + h.add (m.getElementAt (5)); + h.add (m.getElementAt (8)); + + Object defaultValue = new Object(); + LazyListModel lazy = LazyListModel.create (m, new SetFilter (h), 1.0, defaultValue); + lazy.addListDataListener (this); + + for (int cnt = 1; cnt <= 3; cnt++) { + // repeat these tests more times to know whether there is a stability in the results + assertEquals ("Run: " + cnt + " Due to ratio 1.0 the expected size is 10", 10, lazy.getSize ()); + assertEquals ("Run: " + cnt + " zero is zero", new Integer (0), lazy.getElementAt (0)); + assertEquals ("Run: " + cnt + " This will find the closest value, which is 8", new Integer (8), lazy.getElementAt (9)); + assertEquals ("Run: " + cnt + " But then 8 is occupied and we are going to take 5", new Integer (5), lazy.getElementAt (8)); + assertEquals ("Run: " + cnt + " However then we are out of values and we need to return nothing", defaultValue, lazy.getElementAt (7)); + assertEquals ("Run: " + cnt + " Size stays", 10, lazy.getSize ()); + + java.util.HashSet computed = new java.util.HashSet (); + computed.add (new Integer (0)); + computed.add (new Integer (9)); + computed.add (new Integer (8)); + for (int i = 9; i >= 0; i--) { + Integer in = new Integer (i); + if (computed.contains (in)) continue; + + assertEquals ("Run: " + cnt + "No other value has value (" + i + "): ", defaultValue, lazy.getElementAt (i)); + } + } + + lazy.updateYourAssumeptions (); + + assertNextEvent ("First interval is gone", ListDataEvent.INTERVAL_REMOVED, 1, 5); + assertNextEvent ("Middle interval is gone, actually the removed indexes would be 6-8, but" + + " because we already removed 1-5 we should reindex and that is why it is just 2-4", ListDataEvent.INTERVAL_REMOVED, 2, 4); + assertNextEvent ("End is gone, again renumbered", ListDataEvent.INTERVAL_REMOVED, 3, 4); + assertNoEvents (); + + assertEquals ("size 3", 3, lazy.getSize ()); + assertEquals (new Integer (0), lazy.getElementAt (0)); + + } + + public void testBrokenInNumberOf30 () throws Exception { + Model original = new Model (30, false); + EvenOrOdd filter = new EvenOrOdd (true, false); + LazyListModel model = LazyListModel.create (original, filter, 1.0, "Empty"); + + + model.getElementAt (0); + model.getElementAt (1); + model.getElementAt (2); + model.getElementAt (3); + model.getElementAt (4); + model.getElementAt (5); + model.getElementAt (6); + model.getElementAt (7); + model.updateYourAssumeptions (); + model.getElementAt (15); + model.getElementAt (16); + model.getElementAt (17); + model.getElementAt (18); + model.getElementAt (19); + model.getElementAt (20); + model.getElementAt (21); + model.getElementAt (22); + model.updateYourAssumeptions (); + model.getElementAt (11); + model.getElementAt (12); + model.getElementAt (13); + model.getElementAt (14); + model.getElementAt (11); + model.getElementAt (12); + model.getElementAt (13); + model.getElementAt (14); + model.getElementAt (15); + model.getElementAt (16); + model.getElementAt (17); + model.getElementAt (18); + model.updateYourAssumeptions (); + model.getElementAt (11); + model.getElementAt (12); + model.getElementAt (13); + model.getElementAt (14); + model.getElementAt (15); + model.getElementAt (16); + model.getElementAt (9); + model.getElementAt (10); + model.getElementAt (9); + model.getElementAt (10); + model.getElementAt (11); + model.getElementAt (12); + model.getElementAt (13); + model.getElementAt (14); + model.getElementAt (15); + model.getElementAt (16); + model.updateYourAssumeptions (); + model.getElementAt (9); + model.getElementAt (10); + model.getElementAt (11); + model.getElementAt (12); + model.getElementAt (13); + model.getElementAt (14); + model.getElementAt (15); + model.getElementAt (8); + model.getElementAt (8); + model.getElementAt (9); + model.getElementAt (10); + model.getElementAt (11); + model.getElementAt (12); + model.getElementAt (13); + model.getElementAt (14); + model.getElementAt (15); + model.updateYourAssumeptions (); + + assertEquals ("Only half of elements accepted", 15, model.getSize ()); + assertEquals (8 + " th element", new Integer (16), model.getElementAt (8)); + for (int i = 0; i < 15; i++) { + assertEquals (i + " th element", new Integer (i * 2), model.getElementAt (i)); + } + } + + /** Uncomment to test the visually and log for reference + public void testVisual () throws Exception { + Model original = new Model (30, false); + EvenOrOdd filter = new EvenOrOdd (true, true); + LazyListModel model = LazyListModel.create (original, filter, 1.0, "Empty", true); + + + javax.swing.JFrame f = new javax.swing.JFrame ("Testing list"); + f.setDefaultCloseOperation (f.EXIT_ON_CLOSE); + javax.swing.JList list = new javax.swing.JList (model); + list.setPrototypeCellValue ("99"); + f.getContentPane ().add (new javax.swing.JScrollPane (list)); + f.pack (); + f.show (); + + Thread.sleep (1000000); + } + /* */ + + + // + // Events that we get + // + + public void contentsChanged (javax.swing.event.ListDataEvent e) { + events.add (e); + } + + public void intervalAdded (javax.swing.event.ListDataEvent e) { + events.add (e); + } + + public void intervalRemoved (javax.swing.event.ListDataEvent e) { + events.add (e); + } + + private void assertNextEvent (String msg, int type, int lowIndex, int hiIndex) { + if (events.isEmpty ()) { + fail (msg + " list is empty"); + } + ListDataEvent ev = (ListDataEvent)events.removeFirst (); + + assertEquals (msg + " type: ", type, ev.getType ()); + assertEquals (msg + " low: ", lowIndex, ev.getIndex0 ()); + assertEquals (msg + " hi: ", hiIndex, ev.getIndex1 ()); + } + + private void assertNoEvents () { + assertTrue ("No more messages", events.isEmpty ()); + } + + // + // Supporting filters + // + + private static final class SetFilter implements LazyListModel.Filter { + private java.util.Set set; + + public SetFilter (java.util.Set s) { + this.set = s; + } + + public boolean accept (Object o) { + return set.contains (o); + } + + public void scheduleUpdate (Runnable run) { + } + + } + + private static final class EvenOrOdd implements LazyListModel.Filter { + private boolean even; + private boolean update; + + public EvenOrOdd (boolean even) { + this (even, false); + } + public EvenOrOdd (boolean even, boolean update) { + this.even = even; + this.update = update; + } + + public boolean accept (Object o) { + int v = ((Integer)o).intValue (); + return even == (v % 2 == 0); + } + + public void scheduleUpdate (Runnable run) { + if (update) { + javax.swing.SwingUtilities.invokeLater (run); + } + } + + } + + private static final class Model extends javax.swing.AbstractListModel { + private int size; + private boolean[] queried; + private boolean print; + + public Model (int s) { + this (s, false); + } + public Model (int s, boolean print) { + this.print = print; + size = s; + queried = new boolean[size]; + } + + public int getSize() { + return size; + } + + public Object getElementAt(int index) { + queried[index] = true; + if (print) { + System.err.println("created index: " + index); + } + return new Integer (index); + } + + public void assertModel (String msg, LazyListModel model) { + int i = 0; int j = 0; + LazyListModel.Filter filter = model.getFilter (); + while (i < getSize ()) { + if (filter.accept (new Integer (i))) { + // ok, this integer should be in the filtered model + if (model.isComputed (j)) { + // is computed so compare + Object filteredValue = model.getElementAt (j); + assertTrue (j + " is computed, " + i + " must be as well", queried[i]); + assertEquals (i + " and " + j + " should be equal", filteredValue, getElementAt (i)); + } + j++; + } + i++; + } +/* + if (model.getSize () < j) { + assertEquals ("The size should always be smaller than the number of computed elements.", model.getSize (), j); + } + */ + } + } +}