UIForTable.java

package swingtree;

import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import sprouts.Event;
import swingtree.api.Buildable;
import swingtree.api.Configurator;
import swingtree.api.model.BasicTableModel;
import swingtree.api.model.TableListDataSource;
import swingtree.api.model.TableMapDataSource;

import javax.swing.*;
import javax.swing.table.*;
import java.util.*;
import java.util.function.Function;

/**
 *  A SwingTree builder node designed for configuring {@link JTable} instances allowing
 *  for a fluent API to build tables in a declarative way.
 */
public final class UIForTable<T extends JTable> extends UIForAnySwing<UIForTable<T>, T>
{
    private static final Logger log = org.slf4j.LoggerFactory.getLogger(UIForTable.class);

    private final BuilderState<T> _state;


    /**
     * Extensions of the {@link  UIForAnySwing} always wrap
     * a single component for which they are responsible.
     *
     * @param state The {@link BuilderState} modelling how the component is built.
     */
    UIForTable( BuilderState<T> state ) {
        Objects.requireNonNull(state);
        _state = state;
    }

    @Override
    protected BuilderState<T> _state() {
        return _state;
    }
    
    @Override
    protected UIForTable<T> _newBuilderWithState(BuilderState<T> newState ) {
        return new UIForTable<>(newState);
    }

    /**
     *  Use this to set the table header.
     *
     * @param header The table header to be set.
     * @return This builder node.
     */
    public final UIForTable<T> withHeader( UIForTableHeader<?> header ) {
        NullUtil.nullArgCheck(header, "header", UIForTableHeader.class);
        return withHeader(header.getComponent());
    }

    /**
     *  Use this to set the table header.
     *
     * @param header The table header to be set.
     * @return This builder node.
     */
    public final UIForTable<T> withHeader( JTableHeader header ) {
        NullUtil.nullArgCheck(header, "header", JTableHeader.class);
        return _with( thisComponent -> {
                    thisComponent.setTableHeader(header);
                })
                ._this();
    }

    private static <T extends JTable> RenderBuilder<T, Object> _renderTable() {
        return (RenderBuilder) RenderBuilder.forTable(Object.class);
    }

    /**
     *  Use this to build a table cell renderer for a particular column.
     *  The second argument accepts a lambda function which exposes the builder API for a cell renderer.
     *  Here is an example of how to use this method:
     * <pre>{@code
     *     UI.table(myModel)
     *     .withRendererForColumn("column1", it -> it
     *         .when(String.class)
     *         .asText( cell -> "[" + cell.valueAsString().orElse("") + "]" ) )
     *     )
     *     .withRendererForColumn("column2", it -> it
     *         .when(Float.class)
     *         .asText( cell -> "(" + cell.valueAsString().orElse("") + "f)" ) )
     *         .when(Double.class)
     *         .asText( cell -> "(" + cell.valueAsString().orElse("") + "d)" ) )
     *     );
     * }</pre>
     * The above example would render the first column of the table as a string surrounded by square brackets,
     * and the second column as a float or double value surrounded by parentheses.
     * Note that the API allows you to specify how specific types of table entry values
     * should be rendered. This is done by calling the {@link RenderBuilder#when(Class)} method
     * before calling the {@link RenderAs#asText(Function)} method.
     *
     * @param columnName The name of the column for which the cell renderer will be built.
     * @param renderBuilder A lambda function which exposes a fluent builder API for a cell renderer
     *                      and returns the builder API for a cell renderer.
     *                      Call the appropriate methods on the builder API to configure the cell renderer.
     * @return This builder node.
     */
    public final UIForTable<T> withRendererForColumn(
        String columnName,
        Configurator<RenderBuilder<T, Object>> renderBuilder
    ) {
        NullUtil.nullArgCheck(renderBuilder, "renderBuilder", RenderBuilder.class);
        RenderBuilder<T, Object> builder = _renderTable();
        try {
            builder = renderBuilder.configure(builder);
        } catch (Exception e) {
            log.error("Error while building table renderer.", e);
            return this;
        }
        return withCellRendererForColumn(columnName, builder.getForTable());
    }

    /**
     *  Use this to build a table cell renderer for a particular column.
     *  The second argument accepts a lambda function which exposes the builder API for a cell renderer.
     *  Here an example of how this method may be used:
     * <pre>{@code
     *     UI.table(myModel)
     *     .withRendererForColumn(0, it -> it
     *         .when(String.class)
     *         .asText( cell -> "[" + cell.valueAsString().orElse("") + "]" ) )
     *     )
     *     .withRendererForColumn(1, it -> it
     *         .when(Float.class)
     *         .asText( cell -> "(" + cell.valueAsString().orElse("") + "f)" ) )
     *         .when(Double.class)
     *         .asText( cell -> "(" + cell.valueAsString().orElse("") + "d)" ) )
     *     );
     * }</pre>
     * The above example would render the first column of the table as a string surrounded by square brackets,
     * and the second column as a float or double value surrounded by parentheses.
     * Note that the API allows you to specify how specific types of table entry values
     * should be rendered. This is done by calling the {@link RenderBuilder#when(Class)} method
     * before calling the {@link RenderAs#asText(Function)} method.
     *
     * @param columnIndex The index of the column for which the cell renderer will be built.
     * @param renderBuilder A lambda function which exposes a fluent builder API for a cell renderer
     *                      and returns the builder API for a cell renderer.
     *                      Call the appropriate methods on the builder API to configure the cell renderer.
     * @return This builder node.
     */
    public final UIForTable<T> withRendererForColumn(
        int columnIndex,
        Configurator<RenderBuilder<T, Object>> renderBuilder
    ) {
        NullUtil.nullArgCheck(renderBuilder, "renderBuilder", RenderBuilder.class);
        RenderBuilder<T, Object> builder = _renderTable();
        try {
            builder = renderBuilder.configure(builder);
        } catch (Exception e) {
            log.error("Error while building table renderer.", e);
            return this;
        }
        return withCellRendererForColumn(columnIndex, builder.getForTable());
    }

    /**
     * Use this to register a table cell renderer for a particular column.
     * A {@link TableCellRenderer} is a supplier of {@link java.awt.Component} instances which are used to render
     * the cells of a table.
     * <b>Note that in SwingTree, the preferred way of defining a cell renderer for a particular column is through the
     * {@link #withRendererForColumn(String, Configurator)} method, which allows for a more fluent and declarative
     * way of defining cell renderers.</b>
     *
     * @param columnName The name of the column for which the cell renderer will be registered.
     * @param renderer The cell renderer to be registered.
     * @return This builder node, to allow for builder-style method chaining.
     */
    public final UIForTable<T> withCellRendererForColumn( String columnName, TableCellRenderer renderer ) {
        NullUtil.nullArgCheck(columnName, "columnName", String.class);
        NullUtil.nullArgCheck(renderer, "renderer", TableCellRenderer.class);
        return _with( thisComponent -> {
                    thisComponent.getColumn(columnName).setCellRenderer(renderer);
                })
                ._this();
    }

    /**
     * Use this to register a table cell renderer for a particular column. <br>
     * A {@link TableCellRenderer} is a supplier of {@link java.awt.Component} instances which are used to render
     * the cells of a table.
     * <b>Note that in SwingTree, the preferred way of defining a cell renderer for a particular column is through the
     * {@link #withRendererForColumn(int, Configurator)} method, which allows for a more fluent and declarative
     * way of defining cell renderers.</b>
     *
     * @param columnIndex The index of the column for which the cell renderer will be registered.
     * @param renderer The cell renderer to be registered.
     * @return This builder node.
     */
    public final UIForTable<T> withCellRendererForColumn( int columnIndex, TableCellRenderer renderer ) {
        NullUtil.nullArgCheck(renderer, "renderer", TableCellRenderer.class);
        return _with( thisComponent -> {
                    thisComponent.getColumnModel().getColumn(columnIndex).setCellRenderer(renderer);
                })
                ._this();
    }

    /**
     *  Use this to register a table cell renderer for all columns of this table.<br>
     *  A {@link TableCellRenderer} is a supplier of {@link java.awt.Component} instances which are used to render
     *  the cells of a table.<br><br>
     *  <b>Note that in SwingTree, the preferred way of defining a cell renderer is through the
     *  {@link #withRenderer(Configurator)} method, which allows for a more fluent and declarative
     *  way of defining cell renderers.</b>
     *
     * @param renderer A provider of {@link java.awt.Component} instances which are used to render the cells of a table.
     * @return This builder instance, to allow for method chaining.
     */
    public final UIForTable<T> withCellRenderer( TableCellRenderer renderer ) {
        NullUtil.nullArgCheck(renderer, "renderer", TableCellRenderer.class);
        return _with( thisComponent -> {
                    thisComponent.setDefaultRenderer(Object.class, renderer);
                })
                ._this();
    }

    /**
     *  Use this to define a table cell renderer for all columns of this table
     *  using the fluent builder API exposed to the provided lambda function.<br>
     *  Here is an example of how this method is used:
     *  <pre>{@code
     *    UI.table()
     *    .withRenderer( it -> it
     *        .when(SomeDataType.class)
     *        .asText( cell -> cell.value().get().toString() )
     *    )
     *    // ...
     *  }</pre>
     *  You may want to know that a similar API is also available for the {@link javax.swing.JList}
     *  and {@link javax.swing.JComboBox} components, see {@link UIForList#withRenderer(Configurator)},
     *  {@link UIForCombo#withRenderer(Configurator)} for more information.
     *
     *
     * @param renderBuilder A lambda function which exposes the builder API for a cell renderer
     *                      and returns the builder API for a cell renderer.
     *                      Call the appropriate methods on the builder API to configure the cell renderer.
     * @return This builder node.
     */
    public final UIForTable<T> withRenderer(
        Configurator<RenderBuilder<T, Object>> renderBuilder
    ) {
        NullUtil.nullArgCheck(renderBuilder, "renderBuilder", RenderBuilder.class);
        RenderBuilder<T, Object> builder = _renderTable();
        try {
            builder = renderBuilder.configure(builder);
        } catch (Exception e) {
            log.error("Error while building table renderer.", e);
            return this;
        }
        Objects.requireNonNull(builder);
        return withCellRenderer(builder.getForTable());
    }

    /**
     * Use this to register a table cell editor for a particular column.
     * @param columnName The name of the column for which the cell editor will be registered.
     * @param editor The cell editor to be registered.
     * @return This builder instance, to allow for method chaining.
     */
    public final UIForTable<T> withCellEditorForColumn( String columnName, TableCellEditor editor ) {
        NullUtil.nullArgCheck(columnName, "columnName", String.class);
        NullUtil.nullArgCheck(editor, "editor", TableCellEditor.class);
        return _with( thisComponent -> {
                    thisComponent.getColumn(columnName).setCellEditor(editor);
                })
                ._this();
    }

    /**
     * Use this to register a table cell editor for a particular column.
     * @param columnIndex The index of the column for which the cell editor will be registered.
     * @param editor The cell editor to be registered.
     * @return This builder node, to allow for builder-style method chaining.
     */
    public final UIForTable<T> withCellEditorForColumn( int columnIndex, TableCellEditor editor ) {
        NullUtil.nullArgCheck(editor, "editor", TableCellEditor.class);
        return _with( thisComponent -> {
                    thisComponent.getColumnModel().getColumn(columnIndex).setCellEditor(editor);
                })
                ._this();
    }

    /**
     *  Use this to set a table model.
     *  The provided argument is a builder object whose build method will be called
     *  for you instead of having to call the build method on the builder object yourself.
     * @param dataModelBuilder The builder object which will be used to build and then set the table model.
     * @return This builder object.
     */
    public final UIForTable<T> withModel( Buildable<BasicTableModel> dataModelBuilder ) {
        Objects.requireNonNull(dataModelBuilder);
        return this.withModel(dataModelBuilder.build());
    }

    /**
     *  Exposes a fluent builder API for a table model. <br>
     *  Here an example demonstrating how this API
     *  is typically used as part of a UI declaration:
     *  <pre>{@code
     *  UI.table().withModel( m -> m
     *      .colName( col -> new String[]{"X", "Y", "Z"}[col] )
     *      .colCount( () -> 3 )
     *      .rowCount( () -> data.size() )
     *      .getsEntryAt( (r, c) -> data[r][c] )
     *      .updateOn(update)
     *  )
     *  }</pre>
     *  The builder API is exposed to the lambda function passed to this method.
     *  The actually {@link TableModel} is built internally and then set on the table.
     *
     * @param dataModelBuilder A lambda function which receives a builder API for a table model
     * @return This builder instance, to allow for further method chaining.
     */
    public final UIForTable<T> withModel(
        Configurator<BasicTableModel.Builder<Object>> dataModelBuilder
    ) {
        Objects.requireNonNull(dataModelBuilder);
        BasicTableModel.Builder<Object> builder = new BasicTableModel.Builder<>(Object.class);
        try {
            builder = dataModelBuilder.configure(builder);
        } catch (Exception e) {
            log.error("Error while building table model.", e);
        }
        return this.withModel(builder.build());
    }

    /**
     *  Exposes a fluent builder API for a table model holding a specific type of entry. <br>
     *  Here an example demonstrating how this API
     *  is typically used as part of a UI declaration:
     *  <pre>{@code
     *  UI.table().withModel(Double.class, m -> m
     *      .colName( col -> new String[]{"X", "Y", "Z"}[col] )
     *      .colCount( () -> 3 )
     *      .rowCount( () -> data.size() )
     *      .getsEntryAt( (r, c) -> data[r][c] )
     *      .updateOn(update)
     *  )
     *  }</pre>
     *  In this example, the table model is built for a {@link Double} based data source.
     *  So here the data array is a two-dimensional array of {@link Double}s. <br>
     *  <br>
     *  Note that the builder API is exposed to the lambda function passed to this method.
     *  The actual {@link TableModel} is built internally and then installed on the table component.
     *  <p>
     *  You can also use the {@link UI#table(Configurator)} factory method to directly create a table
     *  with a custom table model. <br>
     *
     * @param itemType The type of the table entry {@link Object}s.
     * @param dataModelBuilder A lambda function which receives a builder API for a table model
     * @return This builder instance, to allow for further method chaining.
     */
    public final <E> UIForTable<T> withModel(
        Class<E> itemType,
        Configurator<BasicTableModel.Builder<E>> dataModelBuilder
    ) {
        Objects.requireNonNull(itemType);
        Objects.requireNonNull(dataModelBuilder);
        BasicTableModel.Builder<E> builder = new BasicTableModel.Builder<>(itemType);
        try {
            builder = dataModelBuilder.configure(builder);
        } catch (Exception e) {
            log.error("Error while building table model.", e);
        }
        return this.withModel(builder.build());
    }
    /**
     * Use this to set a basic table model for this table.
     * @param model The model for the table model.
     * @return This builder object.
     */
    public final UIForTable<T> withModel( BasicTableModel model ) {
        NullUtil.nullArgCheck(model, "model", BasicTableModel.class);
        return _with( thisComponent -> {
                    thisComponent.setModel(model);
                })
                ._this();
    }

    /**
     *  Use this instead of {@link JTable#setModel(TableModel)} if your table data can be represented by
     *  either a row major {@link List} of {@link List}s of entry {@link Object}s (a list of rows)      <br>
     *  or a columns major {@link List} of {@link List}s of entry {@link Object}s (a list of columns).  <br>
     *  This method will automatically create a {@link AbstractTableModel} instance for you.
     *  <p>
     *      <b>Please note that when the data of the provided data source changes (i.e. when the data source
     *      is a {@link List} and the list is modified), the table model will not be updated automatically!
     *      Use {@link #updateTableOn(sprouts.Event)} to bind an update {@link sprouts.Event} to the table model.</b>
     *
     * @param mode An enum which configures the layout as well as modifiability of the table in a readable fashion.
     * @param dataSource The {@link TableListDataSource} returning a list matrix which will be used to populate the table.
     * @return This builder node.
     * @param <E> The type of the table entry {@link Object}s.
     */
    public final <E> UIForTable<T> withModel( UI.ListData mode, TableListDataSource<E> dataSource ) {
        boolean isRowMajor = mode.isRowMajor();
        boolean isEditable = mode.isEditable();
        if ( isRowMajor )
            return _with( thisComponent ->
                    thisComponent.setModel(new ListBasedTableModel<E>(isEditable, dataSource)
                    {
                        @Override public int getRowCount() { return getData().size(); }
                        @Override public int getColumnCount() {
                            List<List<E>> data = getData();
                            return ( data.isEmpty() ? 0 : data.get(0).size() );
                        }
                        @Override public @Nullable Object getValueAt(int rowIndex, int columnIndex) {
                            List<List<E>> data = getData();
                            if (isNotWithinBounds(rowIndex, columnIndex)) return null;
                            return data.get(rowIndex).get(columnIndex);
                        }
                        @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
                            List<List<E>> data = getData();
                            if ( !isEditable || isNotWithinBounds(rowIndex, columnIndex) ) return;
                            data.get(rowIndex).set(columnIndex, (E)aValue);
                        }
                    })
                )
                ._this();
        else // isColumnMajor
            return _with( thisComponent ->
                    thisComponent.setModel(new ListBasedTableModel<E>(isEditable, dataSource)
                    {
                        @Override public int getRowCount() {
                            List<List<E>> data = getData();
                            return (data.isEmpty() ? 0 : data.get(0).size());
                        }
                        @Override public int getColumnCount() { return getData().size(); }
                        @Override public @Nullable Object getValueAt( int rowIndex, int columnIndex ) {
                            List<List<E>> data = getData();
                            if ( isNotWithinBounds(rowIndex, columnIndex) ) return null;
                            return data.get(columnIndex).get(rowIndex);
                        }
                        @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
                            List<List<E>> data = getData();
                            if ( !isEditable || isNotWithinBounds(rowIndex, columnIndex) ) return;
                            data.get(columnIndex).set(rowIndex, (E)aValue);
                        }
                    })
                )
                ._this();
    }

    /**
     *  Use this instead of {@link JTable#setModel(TableModel)} if your table data can be represented based
     *  on a map of column names to lists of table entries (basically a column major matrix).  <br>
     *  This method will automatically create a {@link AbstractTableModel} instance for you.
     *  <p>
     *      <b>Please note that when the data of the provided data source changes (i.e. when the data source
     *      is a {@link Map} which gets modified), the table model will not be updated automatically!
     *      Use {@link #updateTableOn(sprouts.Event)} to bind an update {@link sprouts.Event} to the table model.</b>
     *
     * @param mode An enum which configures the modifiability of the table in a readable fashion.
     * @param dataSource The {@link TableMapDataSource} returning a column major map based matrix which will be used to populate the table.
     * @return This builder node.
     * @param <E> The type of the table entry {@link Object}s.
     */
    public final <E> UIForTable<T> withModel( UI.MapData mode, TableMapDataSource<E> dataSource ) {
        return _with( thisComponent -> {
                    thisComponent.setModel(new MapBasedColumnMajorTableModel<>(mode.isEditable(), dataSource));
                })
                ._this();
    }

    /**
     *  Use this to bind an {@link sprouts.Event} to the {@link TableModel} of this table
     *  which will trigger the {@link AbstractTableModel#fireTableDataChanged()} method.
     *  This is useful if you want to update the table when the data source changes.
     *
     * @param event The event to be bound.
     * @return This builder node, for chaining.
     */
    public final UIForTable<T> updateTableOn( Event event ) {
        NullUtil.nullArgCheck(event, "event", Event.class);
        return _with( thisComponent -> {
                    event.subscribe(()->
                        _runInUI(()->{
                            TableModel model = thisComponent.getModel();
                            if ( model instanceof AbstractTableModel ) {
                                // We want the table model update to be as thorough as possible, so we
                                // will fire a table structure changed event, followed by a table data
                                // changed event.
                                ((AbstractTableModel)model).fireTableStructureChanged();
                                ((AbstractTableModel)model).fireTableDataChanged();
                            }
                            else
                                throw new IllegalStateException("The table model is not an AbstractTableModel instance.");
                        })
                    );
                })
                ._this();
    }


    private static abstract class ListBasedTableModel<E> extends AbstractTableModel
    {
        private final TableListDataSource<E> dataSource;
        private final boolean isEditable;

        ListBasedTableModel(boolean isEditable, TableListDataSource<E> dataSource) {
            this.isEditable = isEditable;
            this.dataSource = dataSource;
        }

        @Override public boolean isCellEditable( int rowIndex, int columnIndex ) { return this.isEditable; }

        protected List<List<E>> getData() {
            List<List<E>> data = dataSource.get();
            if ( data == null ) return new ArrayList<>(); // We really don't want null pointer in UIs.
            return data;
        }
        protected boolean isNotWithinBounds(int rowIndex, int colIndex) {
            if ( rowIndex < 0 || rowIndex >= getRowCount()     ) return true;
            if ( colIndex < 0 || colIndex >= getColumnCount()  ) return true;
            return false;
        }
    }


    private abstract static class MapBasedTableModel<E> extends AbstractTableModel
    {
        private final TableMapDataSource<E> dataSource;
        private final boolean isEditable;

        MapBasedTableModel(boolean isEditable, TableMapDataSource<E> dataSource) {
            this.isEditable = isEditable;
            this.dataSource = dataSource;
        }

        protected Map<String, List<E>> getData() {
            Map<String, List<E>> data = dataSource.get();
            if ( data == null ) return Collections.emptyMap(); // We really don't want null pointer in UIs.
            return data;
        }

        @Override
        public @Nullable String getColumnName(int column) {
            List<String> columnNames = new ArrayList<>(getData().keySet());
            if ( column < 0 || column >= columnNames.size() ) return null;
            return columnNames.get(column);
        }

        @Override public boolean isCellEditable( int rowIndex, int columnIndex ) { return this.isEditable; }


        protected boolean isNotWithinBounds(int rowIndex, int colIndex) {
            if ( rowIndex < 0 || rowIndex >= getRowCount()     ) return true;
            if ( colIndex < 0 || colIndex >= getColumnCount()  ) return true;
            return false;
        }

    }

    private static class MapBasedColumnMajorTableModel<E> extends MapBasedTableModel<E>
    {
        MapBasedColumnMajorTableModel(boolean isEditable, TableMapDataSource<E> dataSource) {
            super(isEditable, dataSource);
        }

        @Override
        public int getRowCount() {
            Map<String, List<E>> data = getData();
            return data.values()
                        .stream()
                        .filter(Objects::nonNull) // Again, we don't want null pointer exceptions in UIs.
                        .mapToInt(List::size)
                        .max()
                        .orElse(0);
        }

        @Override
        public int getColumnCount() { return getData().size(); }

        @Override
        public @Nullable Object getValueAt( int rowIndex, int columnIndex ) {
            if ( isNotWithinBounds(rowIndex, columnIndex) ) return null;
            List<E> column = getData().values().stream().skip(columnIndex).findFirst().orElse(null);
            if ( column == null ) return null;
            if ( rowIndex < 0 || rowIndex >= column.size() ) return null;
            return column.get(rowIndex);
        }
    }

}