0

So in my FXML application I have a ListView displaying information from a class using an ObservableList as well as Add and Delete buttons. The add button works as intended. However, I want to be able to select an entry on the list a click the delete button to remove it from the list. Currently, the entry is removed from the list, but it throws an UnsupportedOperationException and prevents and further functionality. My code is as follows (I removed all code unrelated to the list or deletehandler):

public class SonglibController {
@FXML ListView<Song> SongLibrary;
@FXML Button deleteButton;

private ObservableList<Song> obslist;

public void start() {
    obslist = FXCollections.observableArrayList(
            new Song("Song1","Artist2"),
            new Song("Song3", "Artist2",2019,"Album2"),
            new Song("ABC123","1","Artist"),
            new Song("song1","Artist1",2018,"Album8"),
            new Song("test","5","2012")
            );
    SongLibrary.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Song>(){
        @Override
        public void changed(ObservableValue<? extends Song> observable, Song oldValue, Song newValue) {
            if(obslist.contains(oldValue)){
                oldValue.setDetail(0);
                obslist.set(oldIndex, oldValue);
            }
            if(obslist.contains(newValue)){
                newValue.setDetail(1);
                obslist.set(SongLibrary.getSelectionModel().getSelectedIndex(), newValue);
            }
            oldIndex = SongLibrary.getSelectionModel().getSelectedIndex();

        } 
    });
    SongLibrary.setItems(obslist);

}

public void deleteSongHandler(){

    int selectedIndex = SongLibrary.getSelectionModel().getSelectedIndex();

    //unsupportedOperationException occurs here
    obslist.remove(selectedIndex, selectedIndex+1);

    //sort list at the end
    //this sort might not be necessary
    sort();
}

This is the specific error the console provides:

Exception in thread "JavaFX Application Thread" java.lang.UnsupportedOperationException
at java.util.Collections$UnmodifiableList.add(Unknown Source)
at javafx.collections.ListChangeBuilder.nextRemove(ListChangeBuilder.java:208)
at javafx.collections.ListChangeBuilder.nextSet(ListChangeBuilder.java:453)
at javafx.collections.ObservableListBase.nextSet(ObservableListBase.java:115)
at javafx.collections.ModifiableObservableListBase.set(ModifiableObservableListBase.java:162)
at View.SonglibController$1.changed(SonglibController.java:58)
at View.SonglibController$1.changed(SonglibController.java:1)
at com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:182)
at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
at javafx.beans.property.ReadOnlyObjectPropertyBase.fireValueChangedEvent(ReadOnlyObjectPropertyBase.java:74)
at javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:102)
at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:112)
at javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:146)
at javafx.scene.control.SelectionModel.setSelectedItem(SelectionModel.java:102)
at javafx.scene.control.MultipleSelectionModelBase.lambda$new$34(MultipleSelectionModelBase.java:67)
at com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:137)
at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
at javafx.beans.property.ReadOnlyIntegerPropertyBase.fireValueChangedEvent(ReadOnlyIntegerPropertyBase.java:72)
at javafx.beans.property.ReadOnlyIntegerWrapper.fireValueChangedEvent(ReadOnlyIntegerWrapper.java:102)
at javafx.beans.property.IntegerPropertyBase.markInvalid(IntegerPropertyBase.java:113)
at javafx.beans.property.IntegerPropertyBase.set(IntegerPropertyBase.java:147)
at javafx.scene.control.SelectionModel.setSelectedIndex(SelectionModel.java:68)
at javafx.scene.control.MultipleSelectionModelBase.select(MultipleSelectionModelBase.java:404)
at javafx.scene.control.MultipleSelectionModelBase.select(MultipleSelectionModelBase.java:444)
at javafx.scene.control.ListView$ListViewBitSetSelectionModel$2.onChanged(ListView.java:1242)
at javafx.collections.WeakListChangeListener.onChanged(WeakListChangeListener.java:88)
at com.sun.javafx.collections.ListListenerHelper$Generic.fireValueChangedEvent(ListListenerHelper.java:329)
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 com.sun.javafx.collections.ObservableListWrapper.remove(ObservableListWrapper.java:167)
at View.SonglibController.deleteSongHandler(SonglibController.java:139)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at sun.reflect.misc.Trampoline.invoke(Unknown Source)
at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at sun.reflect.misc.MethodUtil.invoke(Unknown Source)
at javafx.fxml.FXMLLoader$MethodHandler.invoke(FXMLLoader.java:1771)
at javafx.fxml.FXMLLoader$ControllerMethodEventHandler.handle(FXMLLoader.java:1657)
at com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:86)
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.EventUtil.fireEventImpl(EventUtil.java:74)
at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:49)
at javafx.event.Event.fireEvent(Event.java:198)
at javafx.scene.Node.fireEvent(Node.java:8411)
at javafx.scene.control.Button.fire(Button.java:185)
at com.sun.javafx.scene.control.behavior.ButtonBehavior.mouseReleased(ButtonBehavior.java:182)
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.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:394)
at com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:295)
at java.security.AccessController.doPrivileged(Native Method)
at com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$353(GlassViewEventHandler.java:432)
at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:389)
at com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:431)
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.win.WinApplication._runLoop(Native Method)
at com.sun.glass.ui.win.WinApplication.lambda$null$147(WinApplication.java:177)
at java.lang.Thread.run(Unknown Source)
 Exception in thread "JavaFX Application Thread" java.lang.UnsupportedOperationException
at java.util.Collections$UnmodifiableList.add(Unknown Source)
at javafx.collections.ListChangeBuilder.insertRemoved(ListChangeBuilder.java:106)
at javafx.collections.ListChangeBuilder.nextRemove(ListChangeBuilder.java:210)
at javafx.collections.ListChangeBuilder.nextSet(ListChangeBuilder.java:453)
at javafx.collections.ObservableListBase.nextSet(ObservableListBase.java:115)
at javafx.collections.ModifiableObservableListBase.set(ModifiableObservableListBase.java:162)
at View.SonglibController$1.changed(SonglibController.java:58)
at View.SonglibController$1.changed(SonglibController.java:1)
at com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:182)
at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
at javafx.beans.property.ReadOnlyObjectPropertyBase.fireValueChangedEvent(ReadOnlyObjectPropertyBase.java:74)
at javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:102)
at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:112)
at javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:146)
at javafx.scene.control.SelectionModel.setSelectedItem(SelectionModel.java:102)
at javafx.scene.control.MultipleSelectionModelBase.lambda$new$34(MultipleSelectionModelBase.java:67)
at com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:137)
at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
at javafx.beans.property.ReadOnlyIntegerPropertyBase.fireValueChangedEvent(ReadOnlyIntegerPropertyBase.java:72)
at javafx.beans.property.ReadOnlyIntegerWrapper.fireValueChangedEvent(ReadOnlyIntegerWrapper.java:102)
at javafx.beans.property.IntegerPropertyBase.markInvalid(IntegerPropertyBase.java:113)
at javafx.beans.property.IntegerPropertyBase.set(IntegerPropertyBase.java:147)
at javafx.scene.control.SelectionModel.setSelectedIndex(SelectionModel.java:68)
at javafx.scene.control.MultipleSelectionModelBase.shiftSelection(MultipleSelectionModelBase.java:294)
at javafx.scene.control.ListView$ListViewBitSetSelectionModel.updateSelection(ListView.java:1377)
at javafx.scene.control.ListView$ListViewBitSetSelectionModel.access$1500(ListView.java:1167)
at javafx.scene.control.ListView$ListViewBitSetSelectionModel$2.onChanged(ListView.java:1248)
at javafx.collections.WeakListChangeListener.onChanged(WeakListChangeListener.java:88)
at com.sun.javafx.collections.ListListenerHelper$Generic.fireValueChangedEvent(ListListenerHelper.java:329)
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 com.sun.javafx.collections.ObservableListWrapper.remove(ObservableListWrapper.java:167)
at View.SonglibController.deleteSongHandler(SonglibController.java:139)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at sun.reflect.misc.Trampoline.invoke(Unknown Source)
at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at sun.reflect.misc.MethodUtil.invoke(Unknown Source)
at javafx.fxml.FXMLLoader$MethodHandler.invoke(FXMLLoader.java:1771)
at javafx.fxml.FXMLLoader$ControllerMethodEventHandler.handle(FXMLLoader.java:1657)
at com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:86)
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.EventUtil.fireEventImpl(EventUtil.java:74)
at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:49)
at javafx.event.Event.fireEvent(Event.java:198)
at javafx.scene.Node.fireEvent(Node.java:8411)
at javafx.scene.control.Button.fire(Button.java:185)
at com.sun.javafx.scene.control.behavior.ButtonBehavior.mouseReleased(ButtonBehavior.java:182)
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.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:394)
at com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:295)
at java.security.AccessController.doPrivileged(Native Method)
at com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$353(GlassViewEventHandler.java:432)
at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:389)
at com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:431)
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.win.WinApplication._runLoop(Native Method)
at com.sun.glass.ui.win.WinApplication.lambda$null$147(WinApplication.java:177)
at java.lang.Thread.run(Unknown Source)

The documentation found here doesn't list this as a throw for the two argument signature of the remove function. Any explanation as to why this exception is thrown is greatly appreciated.

Edit: Added the only @override in my file to the above code snippet.

  • We need to see the anonymous class `View.SonglibController$1` – flakes Feb 12 '19 at 23:59
  • it looks like `ObservableList` is turned into `UnmodifiableList` somewhere. declare it like so: `private final ObservableList obslist = FXCollections.observableArrayList();` then populate it – user2914191 Feb 13 '19 at 00:01
  • @flakes How do I get the anonymous class from eclipse or my file system (windows 8)? – John Strauser Feb 13 '19 at 00:01
  • You created it somewhere. An anonymous class is when you declare some class by implementing its body in line. It will look something like `obslist.subscribe(new MyListener() { public @override blah somemethod() { ... } })` or possibly a lambda: `obslist.subscribe(change -> ... )` – flakes Feb 13 '19 at 00:07
  • Looking at the debug output it looks like its on line 58. You can also use the debugger and step into the method call until you hit that line. See in your stacktrace `View.SonglibController$1.changed(SonglibController.java:58)` – flakes Feb 13 '19 at 00:11
  • I added that bit of code to the post. Is it possibly the selection model having some conflict when the selected item is removed? I think I'm starting to understand what might be causing it. Line 58 is obslist.set(SongLibrary.getSelectionModel().getSelectedIndex(), newValue); It looks to me like there is no newvalue that gets selected causing the error. – John Strauser Feb 13 '19 at 00:13
  • 1
    What are you trying to accomplish in this callback? You're mutating the same list you're listening to. You shouldn't be updating a the source during the callback or you can create an infinite loop; that might be the cause of `UnsupportedOperationException`. Also don't modify the changed value. Straight from the docs "In general it is considered bad practice to modify the observed value in this method." https://docs.oracle.com/javafx/2/api/javafx/beans/value/ChangeListener.html – flakes Feb 13 '19 at 00:22
  • 1
    Yup, looks like that is the error. `ObservableList` employs a safety mechanism to prevent modifying the source list during a callback. See https://stackoverflow.com/a/21085917/3280538 – flakes Feb 13 '19 at 00:30
  • In the changed method I used the setDetail() method to set my listview to display more details about the class displayed, specifically only show the extra details of the one that is currently selected. The plan was for setdetail() to cause the toString of each class to update which would update what is displayed in the listview. This doesn't happen for a reason I don't fully understand, but I was able to use obslist.set(...) to force (admittedly a kind of weird way) the listview to update. – John Strauser Feb 13 '19 at 00:31
  • So the solution would be to add a listchangelistener instead of those obslist.set() lines? – John Strauser Feb 13 '19 at 00:32

2 Answers2

1

Modifying the items list of a ListView may update the selection. This is problematic, since you do it from a listener to the selection. Actually there's little point in setting an element to the current element.

If it's purely the visuals you want to update, you should do this using the ListCells returned from a custom cellFactory.

Example

The following code puts "selected: " in front of selected item's text.

ListView<String> listView = new ListView<>();
listView.getItems().addAll("a", "b", "c");
listView.setCellFactory(lv -> new ListCell<String>() {

    @Override
    protected void updateItem(String item, boolean empty) {
        super.updateItem(item, empty);
        
        if (empty || item == null) {
            setText("");
        } else {
            setText(isSelected() ? "selected: " + item : item.toString());
        }
    }

    @Override
    public void updateSelected(boolean selected) {
        super.updateSelected(selected);
        if (!isEmpty()) {
            String item = getItem();
            setText(selected ? "selected: " + item : item.toString());
        }
    }
    
});
Community
  • 1
  • 1
fabian
  • 80,457
  • 12
  • 86
  • 114
0

To tack on to fabian's answer, if you want a property of the ListView item to trigger an update, then you can use an overload of FXCollections.observableArrayList that binds model properties to the listview.

public class Song {
    BooleanProperty detailProperty ...
}
...

obslist = FXCollections.observableArrayList(
    song -> new Observable[] {song.getDetailProperty()});
obslist.addAll(
    new Song("Song1","Artist2"),
    new Song("Song3", "Artist2",2019,"Album2"),
    new Song("ABC123","1","Artist"),
    new Song("song1","Artist1",2018,"Album8"),
    new Song("test","5","2012"));

In the CellFactory you can look at this property to make a drawing decision.

flakes
  • 21,558
  • 8
  • 41
  • 88