Thursday, October 16, 2014

Per-row mouse cursor in JavaFX TableViews

Today I want to show how to set the mouse cursor in a TableView on a per-row basis.

In a real-world scenario this can be used to indicate what will happen when you click on the row (will it be selected? will it work like a link? is it unselectable?, etc.). To that end it will often be necessary to access the data displayed in the row.

In our example we will set a crosshair cursor for Persons with the letter 'U' in their first name.

First we setup a row factory for the table:

    private void setupPerRowCursor(TableView<Person> table) {
        table.setRowFactory(this::createTableRow);
    }


That row factory will create the row and listen for changes to its indexProperty:

    private TableRow<Person> createTableRow(TableView<Person> table) {
        TableRow<Person> row =  new TableRow<Person>();
        row.indexProperty().addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {
            rowChanged(row, newValue.intValue());
        });
        return row;
    }


Remember that rows (just like cells) will be reused in the table view so even if you have millions of data rows, there will only be a couple of TableRow instances. Therefore we must change react to a row being reused:

    private void rowChanged(TableRow<Person> row, int idx) {
        items = row.getTableView().getItems();
        if (idx >= 0 && idx < items.size()) {
            Person p = items.get(idx);
            if (p.getFirstName().contains("U")) {
                row.setCursor(Cursor.CROSSHAIR);
            }
            else {
                row.setCursor(Cursor.DEFAULT);
            }
        }
    }


We get the (newly associated) data by examining the items of the table. The new index the row points to can be out of bounds, e.g. for TableRows that are displayed but do not have a data row associated, therefore we must check idx to be within limits.

Here's a screenshot with the mouse cursor over a person with the (strange) first name HBZZU:


If you go down with the mouse a bit, the cursor will turn into a normal arrow again.

Here's full source code of the example:
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Cursor;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

import org.apache.commons.lang.RandomStringUtils;

public class TableViewMouseCursorPerRow extends Application {

    ObservableList<Person> items = FXCollections.observableArrayList();
    TableView<Person> table = new TableView<Person>();

    public TableViewMouseCursorPerRow() {
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        GridPane grid = new GridPane();
        table.setItems(items);
        TableColumn<Person, String> firstNameCol = new TableColumn<Person, String>("First Name");
        firstNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("firstName"));
        TableColumn<Person, String> lastNameCol = new TableColumn<Person, String>("Last Name");
        lastNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("lastName"));
        table.getColumns().setAll(firstNameCol, lastNameCol);
        setupPerRowCursor(table);
        addPersons();
        grid.add(table, 0, 1, 3, 1);
       
        Scene scene = new Scene(grid, 400, 400);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void setupPerRowCursor(TableView<Person> table) {
        table.setRowFactory(this::createTableRow);
    }
   
    private TableRow<Person> createTableRow(TableView<Person> table) {
        TableRow<Person> row =  new TableRow<Person>();
        row.indexProperty().addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {
            rowChanged(row, newValue.intValue());
        });
        return row;
    }

    private void rowChanged(TableRow<Person> row, int idx) {
        items = row.getTableView().getItems();
        if (idx >= 0 && idx < items.size()) {
            Person p = items.get(idx);
            if (p.getFirstName().contains("U")) {
                row.setCursor(Cursor.CROSSHAIR);
            }
            else {
                row.setCursor(Cursor.DEFAULT);
            }
        }
    }

    private void addPersons() {
        for (int i = 0; i < 50; i++) {
            Person p = new Person();
            p.setFirstName(items.size() + " " + RandomStringUtils.random(5, "ABEGHILOUZ"));
            p.setLastName(RandomStringUtils.random(10, "QWRTOUPL"));
            items.add(p);
        }
    }

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

    public class Person {
        private StringProperty firstName;

        public void setFirstName(String value) {
            firstNameProperty().set(value);
        }

        public String getFirstName() {
            return firstNameProperty().get();
        }

        public StringProperty firstNameProperty() {
            if (firstName == null)
                firstName = new SimpleStringProperty(this, "firstName");
            return firstName;
        }

        private StringProperty lastName;

        public void setLastName(String value) {
            lastNameProperty().set(value);
        }

        public String getLastName() {
            return lastNameProperty().get();
        }

        public StringProperty lastNameProperty() {
            if (lastName == null)
                lastName = new SimpleStringProperty(this, "lastName");
            return lastName;
        }
    }
}

Tuesday, October 7, 2014

Setting the preferred width of a ListView to fit its items in JavaFX

Let's assume, we want to create a popup listview, that is toggled by a click on a button. Like this:

import java.util.Random;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.layout.GridPane;
import javafx.stage.Popup;
import javafx.stage.Stage;

import org.apache.commons.lang.RandomStringUtils;

public class ListViewWidth extends Application {
   
    Popup popup = new Popup();   
    ListView<String> listView = new ListView<>();
    Button button = new Button("Popup");
    Random rnd = new Random();

    public ListViewWidth() {
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        GridPane grid = new GridPane();
        grid.add(button, 0, 0);
        button.setOnAction(this::toggle);
        popup.getContent().add(listView);
        Scene scene = new Scene(grid, 640, 480);
        primaryStage.setScene(scene);       
        primaryStage.show();
    }
   
    private void toggle(ActionEvent e) {
        if (popup.isShowing()) {
            popup.hide();
        }
        else {
            ObservableList<String> items = createItems();
            listView.setItems(items);
            Point2D pos = button.localToScreen(0, 0);
            popup.show(button, pos.getX(), pos.getY() + button.getHeight());
        }       
    }
   
    private ObservableList<String> createItems() {
        ObservableList<String> result = FXCollections.observableArrayList();
        for (int i = 0; i < 14; i++) {
            result.add(RandomStringUtils.random(rnd.nextInt(100) + 1, true, true));
        }
        return result;
    }

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


Whenever the button is clicked, we hide the popup, if it is already displayed. If it is not yet displayed we create new strings of random length to act as items for the listview and then we show the popup right underneath the button. The screenshot below shows how the popup will look like.


As you can see, we now have an ugly horizontal scrollbar. To get rid of it, we need to set the preferred width of the list view to the width of the widest element in the list, which is a bit tricky (JavaFX own implementation of the ComboBox is doing it with the aid of a few package private methods which we cannot use here).

The trick is to install your own cell factory
         listView.setCellFactory(this::cellFactory);
and make the cells report the sizes to the listview
    private ListCell<String> cellFactory(ListView<String> view) {
        return new ListCell<String>() {
            @Override
            protected void updateItem(String item, boolean empty) {
                super.updateItem(item, empty);
                setText(item);
                widthProperty().addListener(this::widthChanged);
            }

        };
    }


Whenever a cell reports a changed width, we compare it to the current preferred width of the list view and set the preferred width of the list view to the maximum of both values
private void widthChanged(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
    double width = getWidth() + listView.getInsets().getLeft() + listView.getInsets().getRight();
    listView.setPrefWidth(Math.max(listView.getPrefWidth(), width));
}          
 

Note that you have to add the insets of the listview to the required width in order to get the correct size. Then we have a perfect popup listview beneath our button:


This looks good -- at first sight. Problems arise, if the number of list view entries grow to the point that they are no longer displayable together.
In that case, cells will not be created for the invisible rows and thus the maximum size of all the rows is no longer correct. If you scroll down, the listview will suddenly grow wider, if a cell is encountered whose width is bigger than the current maximum.

To make the problem more pronounced, I've changed createItems() to create more items and increase the length of the items that are further down:
    private ObservableList<String> createItems() {
        ObservableList<String> result = FXCollections.observableArrayList();
        for (int i = 0; i < 140; i++) {
            result.add(RandomStringUtils.random(rnd.nextInt(i+5) + 1, true, true));
        }
        return result;
    }



What we really want is to calculate the width of all the rows in the list. This is exactly what the Method prefWidth(...) does for a cell, so we can write something like this:
    private void setPreferredWidth(ObservableList<String> items) {
        ListCell<String> cell = new MyListCell();
        double width = 0.0;
        for (String item : items) {
            cell.setText(item);
            width = Math.max(width, cell.prefWidth(-1));
        }
        listView.setPrefWidth(width + listView.getInsets().getLeft() + listView.getInsets().getRight());
    }


We're using only one cell here to measure the widths of all the rows, calculating the maximum width as we go. Since prefWidth() will only work, if the cell has a skin, we have to play a little trick in the constructor of MyListCell:
    private class MyListCell extends ListCell<String> {
        public MyListCell() {
            super();
            updateListView(listView);
            setSkin(createDefaultSkin());
        }
    }


Now everything works the way we want it:

Well almost. If we scroll all the way down, the largest of the rows will require space that is occupied by the vertical scrollbar and thus a horizontal scrollbar is required:

Also the cells we render have an inset that is not yet taken into account. Unfortunately both the scrollbar as well as the (rendered) cell inset will only be available after rendering, so a slight flicker might be visible with the following solution:

import java.util.Random;

import javafx.application.Application;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.ScrollBar;
import javafx.scene.layout.GridPane;
import javafx.stage.Popup;
import javafx.stage.Stage;

import org.apache.commons.lang.RandomStringUtils;

public class ListViewWidth extends Application {

    Popup popup = new Popup();
    ListView<String> listView = new ListView<>();
    Button button = new Button("Popup");
    Random rnd = new Random();
    double cellInset = 0.0;

    public ListViewWidth() {
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        GridPane grid = new GridPane();
        grid.add(button, 0, 0);
        button.setOnAction(this::toggle);
        popup.getContent().add(listView);
        Scene scene = new Scene(grid, 640, 480);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void toggle(ActionEvent e) {
        if (popup.isShowing()) {
            popup.hide();
        } else {
            ObservableList<String> items = createItems();
            listView.setItems(items);
            listView.setCellFactory(this::cellFactory);
            Point2D pos = button.localToScreen(0, 0);
            popup.show(button, pos.getX(), pos.getY() + button.getHeight());
            double width = getMaxItemWidth(items) + listView.getInsets().getLeft() + listView.getInsets().getRight() + cellInset;
            ScrollBar bar = getVerticalScrollbar();
            width += bar.getWidth();
            listView.setPrefWidth(width);
        }
    }

    private double getMaxItemWidth(ObservableList<String> items) {
        ListCell<String> cell = new MyListCell();
        double width = 0.0;
        for (String item : items) {
            cell.setText(item);
            width = Math.max(width, cell.prefWidth(-1));
        }
        return width;
    }

    private class MyListCell extends ListCell<String> {
        public MyListCell() {
            super();
            updateListView(listView);
            setSkin(createDefaultSkin());
            insetsProperty().addListener(this::insetsChanged);
        }

        @Override
        protected void updateItem(String item, boolean empty) {
            super.updateItem(item, empty);
            setText(item);
        }
       
        private void insetsChanged(ObservableValue<? extends Insets> observable, Insets oldValue, Insets newValue) {
            cellInset = newValue.getLeft() + newValue.getRight();
        }                   
    }

    private ScrollBar getVerticalScrollbar() {
        ScrollBar result = null;
        for (Node n : listView.lookupAll(".scroll-bar")) {
            if (n instanceof ScrollBar) {
                ScrollBar bar = (ScrollBar) n;
                if (bar.getOrientation().equals(Orientation.VERTICAL)) {
                    result = bar;
                }
            }
        }
        return result;
    }

    private ListCell<String> cellFactory(ListView<String> view) {
        return new MyListCell();
    }

    private ObservableList<String> createItems() {
        ObservableList<String> result = FXCollections.observableArrayList();
        for (int i = 0; i < 140; i++) {
            result.add(RandomStringUtils.random(rnd.nextInt(i + 5) + 1, true, true));
        }
        return result;
    }

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

Wednesday, October 1, 2014

Infinite scrolling with JavaFX TableView

Today I want to show how to realise infinite scrolling with JavaFX TableView.

The idea behind infinite scrolling is to retrieve more data once the user has scrolled to the bottom of the currently loaded data.

To realise this, we need to get the vertical ScrollBar of the TableView like this:
    private ScrollBar getVerticalScrollbar(TableView<?> table) {
        ScrollBar result = null;
        for (Node n : table.lookupAll(".scroll-bar")) {
            if (n instanceof ScrollBar) {
                ScrollBar bar = (ScrollBar) n;
                if (bar.getOrientation().equals(Orientation.VERTICAL)) {
                    result = bar;
                }
            }
        }       
        return result;
    }


Note that this method can only be executed after the table view has been rendered.


With this method we can add a listener to the scrollbars value property:
        ScrollBar bar = getVerticalScrollbar(table);
        bar.valueProperty().addListener(this::scrolled);


The listener can now check whether we are at the bottom and add new data (Persons in our case) to the item list of the table view:
    void scrolled(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
        double value = newValue.doubleValue();
        System.out.println("Scrolled to " + value);
        ScrollBar bar = getVerticalScrollbar(table);
        if (value == bar.getMax()) {
            System.out.println("Adding new persons.");
            double targetValue = value * items.size();
            addPersons();
            bar.setValue(targetValue / items.size());
        }
    }

Note the call to bar.setValue() after data has been added. This is neccessary to keep the view at the elements viewed when reaching the bottom.

Here is the complete example:
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.stage.Stage;

import org.apache.commons.lang.RandomStringUtils;

public class TableViewInfiniteScrolling extends Application {
   
    ObservableList<Person> items = FXCollections.observableArrayList();
    TableView<Person> table = new TableView<Person>();

    public TableViewInfiniteScrolling() {
        addPersons();
    }

    private void addPersons() {
        for (int i = 0; i < 50; i++) {
            Person p = new Person();
            p.setFirstName(items.size() + " " + RandomStringUtils.random(5, "ABEGHILOUZ"));
            p.setLastName(RandomStringUtils.random(10, "QWRTOUPL"));
            items.add(p);
        }
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        table.setItems(items);
        TableColumn<Person, String> firstNameCol = new TableColumn<Person, String>("First Name");
        firstNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("firstName"));
        TableColumn<Person, String> lastNameCol = new TableColumn<Person, String>("Last Name");
        lastNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("lastName"));       
        table.getColumns().setAll(firstNameCol, lastNameCol);       
       
        Scene scene = new Scene(table, 400, 400);
        primaryStage.setScene(scene);       
        primaryStage.show();
        ScrollBar bar = getVerticalScrollbar(table);
        bar.valueProperty().addListener(this::scrolled);
    }
   
    private ScrollBar getVerticalScrollbar(TableView<?> table) {
        ScrollBar result = null;
        for (Node n : table.lookupAll(".scroll-bar")) {
            if (n instanceof ScrollBar) {
                ScrollBar bar = (ScrollBar) n;
                if (bar.getOrientation().equals(Orientation.VERTICAL)) {
                    result = bar;
                }
            }
        }       
        return result;
    }
       
    void scrolled(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
        double value = newValue.doubleValue();
        System.out.println("Scrolled to " + value);
        ScrollBar bar = getVerticalScrollbar(table);
        if (value == bar.getMax()) {
            System.out.println("Adding new persons.");
            double targetValue = value * items.size();
            addPersons();
            bar.setValue(targetValue / items.size());
        }
    }
   
    public static void main(String[] args) {
        launch(args);
    }
   
    public class Person {
        private StringProperty firstName;
        public void setFirstName(String value) { firstNameProperty().set(value); }
        public String getFirstName() { return firstNameProperty().get(); }
        public StringProperty firstNameProperty() {
            if (firstName == null) firstName = new SimpleStringProperty(this, "firstName");
            return firstName;
        }
   
        private StringProperty lastName;
        public void setLastName(String value) { lastNameProperty().set(value); }
        public String getLastName() { return lastNameProperty().get(); }
        public StringProperty lastNameProperty() {
            if (lastName == null) lastName = new SimpleStringProperty(this, "lastName");
            return lastName;
        }
    }   
   
}

Wednesday, September 24, 2014

Debugging JavaFX with Linux/X11

When debugging a JavaFX application under Linux there is the Problem of mouse grabbing, i.e. the application under test grabs the mouse and no other application can receive normal GUI events.

This of course makes debugging nearly impossible. After some research I've found a solution in the issue tracker of JavaFX: You have to set the system property (aka VM arguments) -Dglass.disableGrab=true.

Like this in eclipse:

BTW, according to the Java bug database, for AWT applications the same can be achieved by using -Dsun.awt.disablegrab=true but I haven't tried that.

Important caveat:  If you start your application this way, drag and drop will no longer work in your application.

Wednesday, September 17, 2014

JavaFX bindings and tooltips

In this post, I will show two things:
  • How to use JavaFX bindings to set a tooltip
  • How to display tooltips if the widget is disbled
This is our GUI:

Pretty simple so far. Here's the code:

package prv.rli.codetest;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

public class TooltipOnDisabledButton extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        GridPane grid = new GridPane();
        grid.setHgap(20);

        Button button = new Button("Mystery");
        grid.add(button, 0, 0);
       
        CheckBox checkbox = new CheckBox("enabled");
        grid.add(checkbox, 1, 0);
       
        Scene scene = new Scene(grid);
        primaryStage.setScene(scene);       
        primaryStage.show();
    }
   
    public static void main(String[] args) {
        launch(args);
    }
}


Next, I want the "Mystery" button to be enabled/disabled if the checkbox is checked/unchecked.

Thanks to JavaFX bindings this can be done pretty easily, by adding this line right before the scene is created:

button.disableProperty().bind(checkbox.selectedProperty().not());

What this means is that the disableProperty of the button is now kept in sync with the (negated) selectedProperty of the checkbox.

And now, I also want the Button to display a Tooltip which will show the user how to enable/disable the button. This could be done by installing a ChangeListener to the selectedProperty() of the checkbox, but since we want to show (off) bindings, let's create a binding that will take a BooleanProperty and produce a Tooltip.

The whole thing then looks like this:

package prv.rli.codetest;

import javafx.application.Application;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

public class TooltipOnDisabledButton extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        GridPane grid = new GridPane();
        grid.setHgap(20);

        Button button = new Button("Mystery");
        grid.add(button, 0, 0);
       
        CheckBox checkbox = new CheckBox("enabled");
        grid.add(checkbox, 1, 0);
       
        button.disableProperty().bind(checkbox.selectedProperty().not());
        button.tooltipProperty().bind(new TooltipBinding(checkbox.selectedProperty()));
       
        Scene scene = new Scene(grid);
        primaryStage.setScene(scene);       
        primaryStage.show();
    }
   
    private static class TooltipBinding extends ObjectBinding<Tooltip> {
           
        BooleanProperty enabled;
       
        public TooltipBinding(BooleanProperty enabled) {
            bind(enabled);
            this.enabled = enabled;
        }

        @Override
        protected Tooltip computeValue() {
            if (enabled.get()) {
                return new Tooltip("To disable this button, uncheck the box to right.");
            }
            else {
                return new Tooltip("To enable this button, check the box to right.");
            }
        }
    }
   
    public static void main(String[] args) {
        launch(args);
    }
}


The TooltipBinding produces a new Tooltip (we could also just create the two tooltips and return them, saving the garbage collector some work) whenever the bound property is changed. 

And it works like a charm -- well almost. When the button is disabled, no tooltip is displayed. The reason for this is documented in [1] and [2]: Disabled nodes are not sent any mouse events.
To circumvent this, we must wrap the button:
        Button button = new Button("Mystery");
        SplitPane wrapper = new SplitPane();
        wrapper.getItems().add(button);
        grid.add(wrapper, 0, 0);

...
        wrapper.tooltipProperty().bind(new TooltipBinding(checkbox.selectedProperty()));

Note that the wrapper has to be a Control in order to have a tooltipProperty(). The problem with this solution is that we now have an additional border (from the SplitPane) around our button. This can easily be amended by


        wrapper.setStyle("-fx-background-color: transparent");

It seems unnatural howeve to introduce a SplitPane as a Tooltip-Wrapper. A simple pane would be the more natural choice. A Pane however has no tooltip property, therefore we must use Tooltip.install() and install a single tooltip whose textProperty() is bound:

package prv.rli.codetest;

import javafx.application.Application;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;

public class TooltipOnDisabledButton extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        GridPane grid = new GridPane();
        grid.setHgap(20);

        Button button = new Button("Mystery");
        Pane wrapper = new Pane();
        wrapper.getChildren().add(button);
        grid.add(wrapper, 0, 0);
       
        CheckBox checkbox = new CheckBox("enabled");
        grid.add(checkbox, 1, 0);
       
        button.disableProperty().bind(checkbox.selectedProperty().not());
        Tooltip tooltip = new Tooltip();
        tooltip.textProperty().bind(new TooltipBinding(checkbox.selectedProperty()));
        Tooltip.install(wrapper, tooltip);
       
        Scene scene = new Scene(grid);
        primaryStage.setScene(scene);       
        primaryStage.show();
    }
   
    private static class TooltipBinding extends ObjectBinding<String> {
           
        BooleanProperty enabled;
       
        public TooltipBinding(BooleanProperty enabled) {
            bind(enabled);
            this.enabled = enabled;
        }

        @Override
        protected String computeValue() {
            if (enabled.get()) {
                return "To disable this button, uncheck the box to the right.";
            }
            else {
                return "To enable this button, check the box to the right.";
            }
        }
    }
   
    public static void main(String[] args) {
        launch(args);
    }
}


Note that the TooltipBinding now generates a String instead of a whole tooltip.

Sunday, May 26, 2013

java.util.List.subList StackOverflowError

The other day I came across an interesting problem in one of our products with the program throwing:

Exception in thread "main" java.lang.StackOverflowError
    at java.util.ArrayList$SubList.rangeCheckForAdd(ArrayList.java:1119)
    at java.util.ArrayList$SubList.add(ArrayList.java:963)
    at java.util.ArrayList$SubList.add(ArrayList.java:965)

The code responsible for this looks roughly like this:

    String documentSubType = new String();
    List<String> uids = new ArrayList<String>();
    public void setUid(int index, String value, String documentSubType) {
        while (uids.size() < index + 1) {
            uids.add("");
        }
        uids.set(index, value);
       
        if (documentSubType != null && !documentSubType.isEmpty()) {
            this.documentSubType = documentSubType;
            uids = uids.subList(0, index + 1);
        }
    }

On the whole it looks innocent enough so how comes it still seems to make the stack explode?

The answer lies in the (careless) usage of the subList method. The intention was to set the length of the uids list exactly to the index+1 if certain criteria are met.

Unfortunately subList returns a view on the original list (this is documented) which is implemented in AbstractList by use of a pointer to the parent list (this is not documented). When calling certain operations on such a view, the operations are called recursively on the parent list (this is absolutely not documented).
If you call subList on a view you get a view on a view, meaning you now have a parent hierarchy two levels deep.

If you call the setUid method above a few thousand times you get a parent hierarchy a few thousand levels deep which is then used within a recursive method call and - hey presto - there is your stack overflow.

The following snippet shows the problem isolated:

    public static void main(String[] args) {
        List<String> lst = new ArrayList<String>();
        lst.add(""); 
        for (int i = 0; i < 50000; i++) {

            lst = lst.subList(0, 1);
        }
        lst.add("test2");       
    }

Lessons learned: Do NOT use subList recursively (i.e. on a list that was itself created by a call to subList) because doing so will slow down (many parent pointers to walk through) or crash (StackOverflowError) your program.

Monday, December 17, 2012

The Traps of String.split in Java

I just stumbled across a few peculiarities in the split Method of java.lang.String.

We'll start with a simple example:
    String value = "A:B";
    String[] parts = value.split(":");
    System.out.println("Parts has " + parts.length + " elements: " + Arrays.asList(parts));       
No surprises here. We get the output:
    Parts has 2 elements: [A, B]

Now let's try the empty string:
    String value = "";
    String[] parts = value.split(":");
    ...
Since there is no ":" in the empty string, the whole input string is returned:
    Parts has 1 elements: []
 


This may easily let you jump to the conclusion that the returned array of strings will always at least contain one element, since it returns the input if no delimiter is found. But that is not the case:
    String value = ":";
    String[] parts = value.split(":");
    ...
This will give:

    Parts has 0 elements: []

So even though our input string is now longer, the output array has fewer elements. How comes? Well if we split ":" along ":", we get ["", ""], but split() will not by default include trailing empty strings and thus the array returned is []. This seems counterintuitive to me.

The solution is to use the 2 parameter version of split and pass a negative number as the second parameter:
    String value = ":";
    String[] parts = value.split(":", -1);
    ...
which will give:
    Parts has 2 elements: [, ]

With that in mind, we can easily answer the question what the following code will give us:
    String value = "|";
    String[] parts = value.split("|");
    ...

 Since this looks just like the ":"-example above, it must give us an empty array, right?
Wrong. It gives us:
    Parts has 2 elements: [, |]
Whoa, what has happened? The parameter passed to split is actually treated as a regular expression and "|" is the or-operator of regular expressions, so we try to split the input string at every occurence of "nothing or nothing" which boils down to splitting the input string before any character, which gives us an array containing an empty string and the pipe character.
If you do:
    String value = "abc";
    String[] parts = value.split("|");
you'll get:
    Parts has 4 elements: [, a, b, c]
If you really want to split at pipe characters you have to use split("\\|"), since this will escape the regular expression meaning of the pipe character.

To summarize it, there are two caveats:
  • Use the two parameter version of split(), if you want to avoid getting an empty array.
  • Escape characters with special meaning in regular expressions (e.g. . * + [ ] ^ $)