JDK-8152396 : ArrayIndexOutOfBoundsException when listening to selection changes on TreeTableView
  • Type: Bug
  • Component: javafx
  • Sub-Component: controls
  • Affected Version: 8u66
  • Priority: P3
  • Status: Resolved
  • Resolution: Fixed
  • OS: linux_redhat_6.0
  • CPU: x86
  • Submitted: 2016-03-05
  • Updated: 2020-03-02
  • Resolved: 2016-05-19
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 9
9Fixed
Related Reports
Relates :  
Relates :  
Relates :  
Relates :  
Relates :  
Description
FULL PRODUCT VERSION :
java version "1.8.0_66"
Java(TM) SE Runtime Environment (build 1.8.0_66-b17)
Java HotSpot(TM) 64-Bit Server VM (build 25.66-b17, mixed mode)


ADDITIONAL OS VERSION INFORMATION :
Linux knucklepuck 2.6.32-220.el6.x86_64 #1 SMP Tue Dec 6 19:48:22 GMT 2011 x86_64 x86_64 x86_64 GNU/Linux

A DESCRIPTION OF THE PROBLEM :
An ArrayIndexOutOfBoundsException is thrown on the JavaFX Application Thread when a TreeTableView's selection model is updated in response to a TreeItem being expanded. 

The TreeTableView is allowing multiple selections, and my application is trying to select all child nodes of a selected TreeItem when that TreeItem is expanded.

There is a FilteredList that was created from the `getSelectedItems()` ObservableList of the TreeTableView's selection model. The exception is thrown when updating this FilteredList; removing the FilteredList prevents this exception from happening.

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. Select a non-leaf TreeItem that is a child of the root node in the TreeTableView.
2. Rapidly expand/collapse the selected TreeItem by clicking quickly on the expand icon.

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The FilteredList should be properly updated with the selected items in the TreeTableView.
ACTUAL -
An ArrayIndexOutOfBoundsException is thrown when expanding/collapsing a selected TreeItem.

ERROR MESSAGES/STACK TRACES THAT OCCUR :
Change: { [TreeItem [ value: Node 1-1 ]] added at 0 }
Change: { [TreeItem [ value: Node 1-1 ]] added at 0 }
Change: { [TreeItem [ value: Node 1-1 ]] added at 0 }
Change: { [TreeItem [ value: Node 1-1 ]] added at 0 }
Change: { [TreeItem [ value: Node 1-1 ]] added at 0 }
Exception in thread "JavaFX Application Thread" java.lang.ArrayIndexOutOfBoundsException
        at java.lang.System.arraycopy(Native Method)
        at javafx.collections.transformation.FilteredList.addRemove(FilteredList.java:269)
        at javafx.collections.transformation.FilteredList.sourceChanged(FilteredList.java:144)
        at javafx.collections.transformation.TransformationList.lambda$getListener$15(TransformationList.java:106)
        at javafx.collections.WeakListChangeListener.onChanged(WeakListChangeListener.java:88)
        at com.sun.javafx.collections.ListListenerHelper$SingleChange.fireValueChangedEvent(ListListenerHelper.java:164)
        at com.sun.javafx.collections.ListListenerHelper.fireValueChangedEvent(ListListenerHelper.java:73)
        at com.sun.javafx.scene.control.ReadOnlyUnbackedObservableList.callObservers(ReadOnlyUnbackedObservableList.java:75)
        at javafx.scene.control.TreeTableView$TreeTableViewArrayListSelectionModel.handleSelectedCellsListChangeEvent(TreeTableView.java:3312)
        at javafx.scene.control.TreeTableView$TreeTableViewArrayListSelectionModel.lambda$new$125(TreeTableView.java:2313)
        at com.sun.javafx.collections.ListListenerHelper$SingleChange.fireValueChangedEvent(ListListenerHelper.java:164)
        at com.sun.javafx.collections.ListListenerHelper.fireValueChangedEvent(ListListenerHelper.java:73)
        at javafx.collections.ObservableListBase.fireChange(ObservableListBase.java:233)
        at javafx.collections.ListChangeBuilder.commit(ListChangeBuilder.java:482)
        at javafx.collections.ListChangeBuilder.endChange(ListChangeBuilder.java:541)
        at javafx.collections.ObservableListBase.endChange(ObservableListBase.java:205)
        at javafx.collections.transformation.SortedList.sourceChanged(SortedList.java:108)
        at javafx.collections.transformation.TransformationList.lambda$getListener$15(TransformationList.java:106)
        at javafx.collections.WeakListChangeListener.onChanged(WeakListChangeListener.java:88)
        at com.sun.javafx.collections.ListListenerHelper$SingleChange.fireValueChangedEvent(ListListenerHelper.java:164)
        at com.sun.javafx.collections.ListListenerHelper.fireValueChangedEvent(ListListenerHelper.java:73)
        at javafx.collections.ObservableListBase.fireChange(ObservableListBase.java:233)
        at javafx.collections.ListChangeBuilder.commit(ListChangeBuilder.java:482)
        at javafx.collections.ListChangeBuilder.endChange(ListChangeBuilder.java:541)
        at javafx.collections.ObservableListBase.endChange(ObservableListBase.java:205)
        at javafx.collections.ModifiableObservableListBase.addAll(ModifiableObservableListBase.java:102)
        at com.sun.javafx.scene.control.SelectedCellsMap.addAll(SelectedCellsMap.java:146)
        at javafx.scene.control.TreeTableView$TreeTableViewArrayListSelectionModel.selectIndices(TreeTableView.java:2832)
        at javafx.scene.control.MultipleSelectionModel.selectRange(MultipleSelectionModel.java:176)
        at TreeTableViewSample.selectChildrenOfRows(TreeTableViewSample.java:96)
        at TreeTableViewSample.lambda$start$1(TreeTableViewSample.java:64)
        at com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:361)
        at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
        at javafx.beans.property.ReadOnlyIntegerWrapper$ReadOnlyPropertyImpl.fireValueChangedEvent(ReadOnlyIntegerWrapper.java:176)
        at javafx.beans.property.ReadOnlyIntegerWrapper.fireValueChangedEvent(ReadOnlyIntegerWrapper.java:142)
        at javafx.beans.property.IntegerPropertyBase.markInvalid(IntegerPropertyBase.java:113)
        at javafx.beans.property.IntegerPropertyBase.set(IntegerPropertyBase.java:147)
        at javafx.scene.control.TreeTableView.setExpandedItemCount(TreeTableView.java:1088)
        at javafx.scene.control.TreeTableView.updateExpandedItemCount(TreeTableView.java:1861)
        at javafx.scene.control.TreeTableView.getExpandedItemCount(TreeTableView.java:1092)
        at com.sun.javafx.scene.control.behavior.TreeTableCellBehavior.getItemCount(TreeTableCellBehavior.java:72)
        at com.sun.javafx.scene.control.behavior.TableCellBehaviorBase.doSelect(TableCellBehaviorBase.java:110)
        at com.sun.javafx.scene.control.behavior.CellBehaviorBase.mouseReleased(CellBehaviorBase.java:159)
        at com.sun.javafx.scene.control.skin.BehaviorSkinBase$1.handle(BehaviorSkinBase.java:96)
        at com.sun.javafx.scene.control.skin.BehaviorSkinBase$1.handle(BehaviorSkinBase.java:89)
        at com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:218)
        at com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)
        at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
        at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
        at com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
        at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
        at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
        at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
        at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
        at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
        at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
        at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
        at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
        at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
        at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
        at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
        at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
        at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
        at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
        at javafx.event.Event.fireEvent(Event.java:198)
        at javafx.scene.Scene$MouseHandler.process(Scene.java:3757)
        at javafx.scene.Scene$MouseHandler.access$1500(Scene.java:3485)
        at javafx.scene.Scene.impl_processMouseEvent(Scene.java:1762)
        at javafx.scene.Scene$ScenePeerListener.mouseEvent(Scene.java:2494)
        at com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:352)
        at com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:275)
        at java.security.AccessController.doPrivileged(Native Method)
        at com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$354(GlassViewEventHandler.java:388)
        at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:389)
        at com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:387)
        at com.sun.glass.ui.View.handleMouseEvent(View.java:555)
        at com.sun.glass.ui.View.notifyMouse(View.java:937)
        at com.sun.glass.ui.gtk.GtkApplication._runLoop(Native Method)
        at com.sun.glass.ui.gtk.GtkApplication.lambda$null$49(GtkApplication.java:139)
        at java.lang.Thread.run(Thread.java:745)

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
import java.util.*;

import javafx.application.Application;
import javafx.collections.*;
import javafx.collections.transformation.*;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.control.TreeTableColumn.CellDataFeatures;
import javafx.stage.Stage;

public class TreeTableViewSample extends Application {

    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage stage) {
        final TreeItem<String> childNode1 = new TreeItem<>("Child Node 1");
        childNode1.getChildren().addAll(
            new TreeItem<String>("Node 1-1"),
            new TreeItem<String>("Node 1-2")
        );

        final TreeItem<String> root = new TreeItem<>("Root node");
        root.setExpanded(true);
        root.getChildren().add(childNode1);

        TreeTableColumn<String,String> column = new TreeTableColumn<>("Column");
        column.setPrefWidth(190);
        column.setCellValueFactory((CellDataFeatures<String, String> p) ->
            new ReadOnlyStringWrapper(p.getValue().getValue()));

        final TreeTableView<String> treeTableView = new TreeTableView<>(root);
        treeTableView.getColumns().add(column);
        treeTableView.setPrefWidth(200);
        treeTableView.setShowRoot(true);
        treeTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);

        // Select all children of expanded node.
        treeTableView.expandedItemCountProperty().addListener((observable, oldCount, newCount) -> {
            if (newCount.intValue() > oldCount.intValue()) {
                selectChildrenOfRows(treeTableView, treeTableView.getSelectionModel().getSelectedIndices());
            }
        });

        filteredList = treeTableView.getSelectionModel().getSelectedItems().filtered(Objects::nonNull);
        filteredList.addListener((ListChangeListener.Change<? extends TreeItem<String>> change) -> {
            System.out.printf("Change: %s\n", change);
        });

        final Scene scene = new Scene(new Group(), 200, 400);
        Group sceneRoot = (Group)scene.getRoot();
        sceneRoot.getChildren().add(treeTableView);

        stage.setTitle("Tree Table View Samples");
        stage.setScene(scene);
        stage.show();
    }

    private FilteredList<TreeItem<String>> filteredList;

    private void selectChildrenOfRows(TreeTableView<String> table, Collection<Integer> selectedRows) {
        for (int index: selectedRows) {
            TreeItem<String> item = table.getTreeItem(index);

            if (item != null && item.isExpanded() && !item.getChildren().isEmpty()) {
                int startIndex = index + 1;
                int maxCount   = startIndex + item.getChildren().size();

                table.getSelectionModel().selectRange(startIndex, maxCount);
            }
        }
    }
}

---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
Don't create a FilteredList from the selectedItems property of a TreeTableView.


Comments
Please file a new issue, since you are still seeing problems. Update: I see you already did file JDK-8189354
16-10-2017

seems to be "fixed" only for 2 child items (in fx9-ea-180) ... increase the # of items and see still the IOOB after a size-dependent # of expands: 3 items -> throws at 5 expands 4 items -> throws at 3 expands
15-10-2017

Cleaning up this code has been a multi-day effort, where I split the issue up into a few related issues as I fixed them (see JDK-8157285 and JDK-8157205). Therefore, the changeset I am now pushing to resolve this specific issue is relatively small in comparison. I've managed to create unit tests for these issues, so the chances of regressions are slim. Changeset: http://hg.openjdk.java.net/openjfx/9-dev/rt/rev/ffdbe7f70194
19-05-2016

there's already an issue (more general, though: dump MultipleSelectionModelBase, see JDK-8088688 ) with a proposal bigger than a mere patch. To make it fly requires work, as in: somebody should pay to get it done :-)
12-04-2016

I'm OK with getting rid of ReadOnlyUnbackedObservableList - it does seem like ObservableListBase is a better fit. I had a quick look at places where ROUOL is being used: 1) MultipleSelectionModelBase: selectedItemsSeq uses it to return the selectedItems based on the selectedIndices list. There is a listener on selectedIndicesSeq so that when it changes the event can be fired out to selectedItems listeners. 2) TableView and TreeView: selectedItemsSeq and selectedCellsSeq are both the same. I think creating a new Jira issue to remove the ROUOL and use ObservableListBase is a good first step to simplifying the selection models. Perhaps you'll even find time to propose a patch ;-)
11-04-2016

I meant ObservableListBase: it has semantics for building up changes by type/location in steps as small as needed and send them out when all changes are accounted for. You can implement such a list based on a BitSet, and add api to add/set/clear indices as needed. Something like public class IndicesBase extends ObservableListBase { BitSet bitSet; public void addIndices(int... indices) { // starts the change builder beginChange(); doAddIndices(indices); // tells the change builder to fire the combined changes endChange(); } protected void doAddIndices(int... indices) { for (int i : indices) { if (bitSet.get(i)) continue; bitSet.set(i); int from = indexOf(i); // adds the next change bit nextAdd(from, from + 1); } } The change builder takes care of all the dirty details of change contract - the only thingy to take care of ourselves is to make sure that the given from is at the _current_ state of the list. Now think of this basic list as a kind-of transformList (can't extend TransformList because we want to re-use for TreeItems) into a backing structure of items: the transform is to "pick" item positions. As a transform, it will listen to changes in that backing structure, update itself and notify its listeners. The change fired by the transform will come out naturally, simply using the semantic change builder methods without having to care about the dirty details. Rather functional code (for list/treeSelection - didn't try table yet, there are limits to my time as well ;-) is in my git https://github.com/kleopatra/swingempire-fx/tree/master9/fx8-swingempire/src/java/de/swingempire/fx/collection
11-04-2016

additional observations: - no need to quickly expand/collapse, the error is thrown regularly at the n-th expand - n depends on the number of children childCount - childCount = 2 -> n = 6, childCount = 3 -> n = 4, childCount => 4 -> n = 3 - same error for TreeView selection as nearly always with selectionModel related issues, the underlying reason is the incorrect book-keeping of selection state and/or incorrect listChange notification. Here it's both/the latter for TreeTable/TreeView respectively. These errors bubble up whenever a ListChangeListener relies on both being correct (as FilteredList does - must do!) Pretty sure that there are already bug reports about them, didn't search though. For a quick check to see the issues: - register a ListChangeListener to the selectedItems which pretty-prints the change - add a else-block to the listener of the expandedItemsCount to see the selectedItems on collapsing treeTableView.expandedItemCountProperty().addListener((observable, oldCount, newCount) -> { if (newCount.intValue() > oldCount.intValue()) { System.out.println("expandedItems/expansionCount: " + newCount + " / " + count++); selectChildrenOfRows(treeTableView, treeTableView.getSelectionModel().getSelectedIndices()); } else { System.out.println("collapsed, selectedItems: " + treeTableView.getSelectionModel().getSelectedItems()); } }); // listener to selectedItems new FXUtils.PrintingListChangeListener("selectedItems", treeTableView.getSelectionModel().getSelectedItems()); The output for TreeTableView with 4 grandChildren in the sequence of select-child/expand-child/collapse-child // select Change #0 on selectedItems list = [TreeItem [ value: Child Node 1 ]] Change event data on list: [TreeItem [ value: Child Node 1 ]] class com.sun.javafx.collections.MappingChange { [TreeItem [ value: Child Node 1 ]] added at 0, } cursor = 0 Kind of change: added Affected range: [0, 1] Added size: 1 Added sublist: [TreeItem [ value: Child Node 1 ]] //-- expand expandedItems/expansionCount: 6 / 0 Change #1 on selectedItems list = [TreeItem [ value: Child Node 1 ], TreeItem [ value: Node 1-1 ], TreeItem [ value: Node 1-2 ], TreeItem [ value: Node 1-3 ], TreeItem [ value: Node 1-4 ]] Change event data on list: [TreeItem [ value: Child Node 1 ], TreeItem [ value: Node 1-1 ], TreeItem [ value: Node 1-2 ], TreeItem [ value: Node 1-3 ], TreeItem [ value: Node 1-4 ]] class com.sun.javafx.collections.MappingChange { [TreeItem [ value: Node 1-1 ], TreeItem [ value: Node 1-2 ], TreeItem [ value: Node 1-3 ], TreeItem [ value: Node 1-4 ]] added at 1, } cursor = 0 Kind of change: added Affected range: [1, 5] Added size: 4 Added sublist: [TreeItem [ value: Node 1-1 ], TreeItem [ value: Node 1-2 ], TreeItem [ value: Node 1-3 ], TreeItem [ value: Node 1-4 ]] Change on filtered: { [TreeItem [ value: Node 1-1 ], TreeItem [ value: Node 1-2 ], TreeItem [ value: Node 1-3 ]] added at 0 } // --- collapse: ERROR collapsed, selectedItems: [TreeItem [ value: Child Node 1 ], null, null, null, null] The error that's confusing the FilteredList is on collapse and two-fold - state is incorrect on having nulls in the selectedItems - notification is incorrect in not firing a removed Similarly setup for a TreeView, same exception thrown but slightly different error: // select Change #0 on selectedItems list = [TreeItem [ value: Child Node 1 ]] Change event data on list: [TreeItem [ value: Child Node 1 ]] class com.sun.javafx.collections.MappingChange { [TreeItem [ value: Child Node 1 ]] added at 0, } cursor = 0 Kind of change: added Affected range: [0, 1] Added size: 1 Added sublist: [TreeItem [ value: Child Node 1 ]] // expand expandedItems/expansionCount: 6 / 0 Change #1 on selectedItems list = [TreeItem [ value: Child Node 1 ], TreeItem [ value: Node 1-1 ], TreeItem [ value: Node 1-2 ], TreeItem [ value: Node 1-3 ], TreeItem [ value: Node 1-4 ]] Change event data on list: [TreeItem [ value: Child Node 1 ], TreeItem [ value: Node 1-1 ], TreeItem [ value: Node 1-2 ], TreeItem [ value: Node 1-3 ], TreeItem [ value: Node 1-4 ]] class com.sun.javafx.collections.MappingChange { [TreeItem [ value: Node 1-1 ], TreeItem [ value: Node 1-2 ], TreeItem [ value: Node 1-3 ], TreeItem [ value: Node 1-4 ]] added at 1, } cursor = 0 Kind of change: added Affected range: [1, 5] Added size: 4 Added sublist: [TreeItem [ value: Node 1-1 ], TreeItem [ value: Node 1-2 ], TreeItem [ value: Node 1-3 ], TreeItem [ value: Node 1-4 ]] //---- collapsed: correct selectedItems collapsed, selectedItems: [TreeItem [ value: Child Node 1 ]] // ------ Error: incorrect change notification Change #2 on selectedItems list = [TreeItem [ value: Child Node 1 ]] Change event data on list: [TreeItem [ value: Child Node 1 ]] class com.sun.javafx.collections.MappingChange { [TreeItem [ value: Child Node 1 ]] added at 0, } cursor = 0 Kind of change: added Affected range: [0, 1] Added size: 1 Added sublist: [TreeItem [ value: Child Node 1 ]] Notification errors - missing removed - incorrect added (nothing changed, the item was selected before and still is) BTW, did I ever mention that getting the list change notification correct is not trivial, such that implementing it from scratch is a moving bullet ;-) Again: since fx-8, there's full support for firing the correct events, no longer any need for UnbackedXX with manually building change events and fire them manually with callObservers ... instead, back them with whatver data structure and call the semantic support methods which internally build the correct change event. Just saying .. (edited to cleanup mixed up rows ..)
11-04-2016

Your last paragraph talks about not needing the UnbackedXX class and using the 'semantic support methods' - can you clarify what you mean here?
11-04-2016