# GridHelper.java

    /*
    ** Module   : GridHelper.java
    ** Abstract : Grid helper.
    **
    ** Copyright (c) 2017-2023, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --------------------------------Description-----------------------------------
    ** 001 HC  20170612 Initial version.
    ** 002 CA  20180607 Changed to allow HTML table cells.
    ** 003 GBB 20231117 Support for column width.
    */
    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */
    package com.goldencode.p2j.admin.client.widget;

    import com.google.common.base.Predicate;
    import com.google.common.collect.*;
    import com.google.gwt.cell.client.*;
    import com.google.gwt.core.client.*;
    import com.google.gwt.dom.client.*;
    import com.google.gwt.regexp.shared.*;
    import com.google.gwt.resources.client.*;
    import com.google.gwt.safehtml.shared.*;
    import com.google.gwt.user.cellview.client.*;
    import com.google.gwt.user.client.ui.*;
    import com.google.gwt.view.client.*;
    import org.gwtbootstrap3.client.ui.gwt.DataGrid;

    import java.util.*;
    import java.util.function.*;
    import java.util.function.Function;

    /**
     * The class makes work with the GWT's grid more pleasant.
     */
    public class GridHelper
    {
       /**
        * Creates a new default instance of {@link DataGrid}.
        *
        * @param   <TRowObject>
        *          The row type.
        *
        * @return  see above
        */
       public static <TRowObject> DataGrid<TRowObject> create()
       {
          return new DataGrid<>(Integer.MAX_VALUE,
                                new ResourcesAdapter(GWT.create(DataGrid.Resources.class)));
       }

       /**
        * Initializes an instance of {@link DataGrid}. The initialization includes creation of columns
        * according to the column definitions, creation of {@link ListDataProvider}, creation of
        * default (single) selection model, setting up sorting and wrapping everything in
        * a {@link GridHandle} which the method returns.
        *
        * @param   grid
        *          The grid to init.
        * @param   columns
        *          The column definitions.
        * @param   <TRowObject>
        *          The row type.
        *
        * @return  see above
        */
       public static <TRowObject> GridHandle<TRowObject>
       initListGrid(DataGrid grid, ColumnInfo<TRowObject> ... columns)
       {
          return initListGrid(grid, false, columns);
       }

       /**
        * Initializes an instance of {@link DataGrid}. The initialization includes creation of columns
        * according to the column definitions, creation of {@link ListDataProvider}, creation of
        * the selection model, setting up sorting and wrapping everything in
        * a {@link GridHandle} which the method returns.
        *
        * @param   grid
        *          The grid to init.
        * @param   multiSelection
        *          Multi selection flag.
        * @param   columns
        *          The column definitions.
        * @param   <TRowObject>
        *          The row type.
        *
        * @return  see above
        */
       public static <TRowObject> GridHandle<TRowObject>
       initListGrid(DataGrid grid, boolean multiSelection, ColumnInfo<TRowObject> ... columns)
       {
          return initListGrid(grid, multiSelection, true, columns);
       }

       /**
        * Initializes an instance of {@link DataGrid}. The initialization includes creation of columns
        * according to the column definitions, creation of {@link ListDataProvider}, creation of
        * the selection model, setting up sorting and wrapping everything in
        * a {@link GridHandle} which the method returns.
        *
        * @param   grid
        *          The grid to init.
        * @param   multiSelection
        *          Multi selection flag.
        * @param   sortable
        *          Sortable flag. When <code>false</code> none of the columns will be made sortable.
        * @param   columns
        *          The column definitions.
        * @param   <TRowObject>
        *          The row type.
        *
        * @return  see above
        */
       public static <TRowObject> GridHandle<TRowObject>
       initListGrid(DataGrid grid,
                    boolean multiSelection,
                    boolean sortable,
                    ColumnInfo<TRowObject> ... columns)
       {
          ListDataProvider<TRowObject> dataProvider = new ListDataProvider<>();
          dataProvider.addDataDisplay(grid);

          ColumnSortEvent.ListHandler<TRowObject> sortHandler = null;

          for (ColumnInfo<TRowObject> column : columns)
          {
             Column<TRowObject, ?> gridCol = null;
             if (column.isHtmlCell())
             {
                gridCol = new Column<TRowObject, SafeHtml>(new SafeHtmlCell()
                {
                   @Override
                   public Set<String> getConsumedEvents()
                   {
                      HashSet<String> events = new HashSet<>();
                      events.add("dblclick");
                      return events;
                   }
                })
                {
                   @Override
                   public SafeHtml getValue(final TRowObject object)
                   {
                      return (SafeHtml) column.getValue(object);
                   }

                   @Override
                   public void onBrowserEvent(Cell.Context context, Element elem,
                                              TRowObject object, NativeEvent event)
                   {
                      super.onBrowserEvent(context, elem, object, event);
                      if (column.dblclickHandler != null && "dblclick".equals(event.getType()))
                      {
                         column.dblclickHandler.accept(object);
                      }
                   }
                };
             }
             else
             {
                gridCol = new Column<TRowObject, String>(new TextCell()
                {
                   @Override
                   public Set<String> getConsumedEvents()
                   {
                      HashSet<String> events = new HashSet<>();
                      events.add("dblclick");
                      return events;
                   }
                })
                {
                   @Override
                   public String getValue(final TRowObject object)
                   {
                      return (String) column.getValue(object);
                   }

                   @Override
                   public void onBrowserEvent(Cell.Context context, Element elem,
                                              TRowObject object, NativeEvent event)
                   {
                      super.onBrowserEvent(context, elem, object, event);
                      if (column.dblclickHandler != null && "dblclick".equals(event.getType()))
                      {
                         column.dblclickHandler.accept(object);
                      }
                   }
                };
             }

             if (!sortable)
             {
                grid.addColumn(gridCol, column.getHeaderLabel());
                continue;
             }

             boolean colSortable = column.getComparator() != null;
             gridCol.setSortable(colSortable);
             grid.addColumn(gridCol, column.getHeaderLabel());

             if (colSortable)
             {
                if (sortHandler == null)
                {
                   sortHandler = new ColumnSortEvent.ListHandler<>(dataProvider.getList());

                   // make the first sortable column sorted
                   ColumnSortList columnSortList = grid.getColumnSortList();
                   columnSortList.push(new ColumnSortList.ColumnSortInfo(gridCol, true));
                }

                sortHandler.setComparator(gridCol, (o1, o2) -> column.getComparator().apply(o1, o2));
                grid.addColumnSortHandler(sortHandler);
             }

             if (column.widthInPixels > 0)
             {
                grid.setColumnWidth(gridCol, column.widthInPixels, Style.Unit.PX);
             }
             else if (column.widthInPercents > 0)
             {
                grid.setColumnWidth(gridCol, column.widthInPercents, Style.Unit.PCT);
             }
          }

          SetSelectionModel<TRowObject> selModel = multiSelection ?
                                                   new MultiSelectionModel<>() :
                                                   new SingleSelectionModel<>();

          grid.setSelectionModel(selModel);
          grid.setHover(true);
          return new GridHandle<>(grid, dataProvider, selModel);
       }

       /**
        * Setups the given grid data provider with new data that satisfy the given filter criteria.
        *
        * @param    <TRowObject>
        *           The row type
        * @param    dataGrid
        *           The given grid
        * @param    dataProvider
        *           The given grid data provider
        * @param    records
        *           The new data
        * @param    filterExp
        *           The given filter criteria
        */
       public static <TRowObject> void updateDataProvider(DataGrid<TRowObject> dataGrid,
                                                          ListDataProvider<TRowObject> dataProvider,
                                                          TRowObject[] records, RegExp filterExp)
       {
          dataProvider.getList().clear();

          Predicate<TRowObject> filterRecords = new Predicate<TRowObject>()
          {

             @Override
             public boolean apply(TRowObject record)
             {
                if (filterExp == null)
                {
                   return true;
                }

                for (int i = 0; i < dataGrid.getColumnCount(); i++)
                {
                   Column<TRowObject, ?> column = dataGrid.getColumn(i);
                   if (filterExp.test(column.getValue(record).toString()))
                   {
                      return true;
                   }
                }

                return false;
             }
          };

          dataProvider.getList().addAll(new ArrayList<TRowObject>(
          Collections2.filter(Arrays.asList(records), filterRecords)));

          dataProvider.flush();

          ColumnSortEvent.fire(dataGrid, dataGrid.getColumnSortList());
       }

       /**
        * A helper method that updates the data provider with the supplied records.
        *
        * @param   dataGrid
        *          The grid instance.
        * @param   dataProvider
        *          The related data provider.
        * @param   records
        *          The new records to insert into the data provider.
        * @param   <TRowObject>
        *          The row type.
        */
       public static <TRowObject> void updateDataProvider(DataGrid<TRowObject> dataGrid,
                                                          ListDataProvider<TRowObject> dataProvider,
                                                          TRowObject[] records)
       {
          dataProvider.getList().clear();

          if (records == null)
          {
             return;
          }

          for (int i = 0; i < records.length; i++)
          {
             dataProvider.getList().add(records[i]);
          }

          dataProvider.flush();

          ColumnSortEvent.fire(dataGrid, dataGrid.getColumnSortList());
       }

       /**
        * Holds grid column definition. The class is used as the source of information when
        * initializing a {@link DataGrid} using the GridHelper.initListGrid methods.
        *
        * @param    <TRowObject>
        *           The row type.
        */
       public static class ColumnInfo<TRowObject>
       {
          /** Header label */
          private String headerLabel;

          /** Value getter */
          private Function<TRowObject, ?> valueGetter;

          /** Row comparator */
          private BiFunction<TRowObject, TRowObject, Integer> comparator;

          /** Double click handler */
          private Consumer<TRowObject> dblclickHandler;

          /** Flag indentifying this cell contains HTML code. */
          private boolean htmlCell = false;

          /** Width in pixels */
          private int widthInPixels;

          /** Width in percents */
          private double widthInPercents;

          /**
           * Ctor.
           *
           * @param   headerLabel
           *          Column header label.
           * @param   valueGetter
           *          Value getter.
           * @param   htmlCell
           *          Flag marking this column as containing HTML code.
           */
          public ColumnInfo(String headerLabel,
                            Function<TRowObject, SafeHtml> valueGetter,
                            boolean htmlCell)
          {
             this.headerLabel = headerLabel;
             this.valueGetter = valueGetter;
             this.comparator = null;
             this.dblclickHandler = null;
             initComparator();

             this.htmlCell = htmlCell;
          }

          /**
           * Ctor.
           *
           * @param   headerLabel
           *          Column header label.
           * @param   valueGetter
           *          Value getter.
           */
          public ColumnInfo(String headerLabel, Function<TRowObject, String> valueGetter)
          {
             this(headerLabel, valueGetter, null, null);
          }

          /**
           * Ctor.
           *
           * @param   headerLabel
           *          Column header label.
           * @param   valueGetter
           *          Value getter.
           * @param   comparator
           *          Row comparator.
           */
          public ColumnInfo(String headerLabel,
                            Function<TRowObject, String> valueGetter,
                            BiFunction<TRowObject, TRowObject, Integer> comparator)
          {
             this(headerLabel, valueGetter, comparator, null);
          }

          /**
           * Ctor.
           *
           * @param   headerLabel
           *          Column header label.
           * @param   valueGetter
           *          Value getter.
           * @param   comparator
           *          Row comparator.
           * @param   dblclickHandler
           *          Doubleclick handler.
           */
          public ColumnInfo(String headerLabel,
                            Function<TRowObject, String> valueGetter,
                            BiFunction<TRowObject, TRowObject, Integer> comparator,
                            Consumer<TRowObject> dblclickHandler)
          {
             this.headerLabel = headerLabel;
             this.valueGetter = valueGetter;
             this.comparator = comparator;
             this.dblclickHandler = dblclickHandler;
             initComparator();
          }

          /**
           * Get the state of the {@link #htmlCell} flag.
           *
           * @return   See above.
           */
          public boolean isHtmlCell()
          {
             return htmlCell;
          }

          /**
           * Returns the column header label.
           *
           * @return  see above
           */
          public String getHeaderLabel()
          {
             return headerLabel;
          }

          /**
           * Use the {@link #valueGetter} to compute the value for this column.
           *
           * @param    object
           *           The object with the rows's data.
           *
           * @return   The computed data for this column.
           */
          public Object getValue(TRowObject object)
          {
             return valueGetter.apply(object);
          }

          /**
           * Returns column width in pixels.
           *
           * @return   See above.
           */
          public int getWidthInPixels()
          {
             return widthInPixels;
          }

          /**
           * Sets column width in pixels.
           *
           * @param    widthInPixels
           *           Column width in pixels.
           */
          public void setWidthInPixels(int widthInPixels)
          {
             this.widthInPixels = widthInPixels;
          }

          /**
           * Returns column width in %.
           *
           * @return   See above.
           */
          public double getWidthInPercents()
          {
             return widthInPercents;
          }

          /**
           * Sets column width in %.
           *
           * @param    widthInPercents
           *           Column width in %.
           */
          public void setWidthInPercents(double widthInPercents)
          {
             this.widthInPercents = widthInPercents;
          }

          /**
           * Returns the row comparator.
           *
           * @return  see above
           */
          public BiFunction<TRowObject, TRowObject, Integer> getComparator()
          {
             return comparator;
          }

          /**
           * Initializes the default row comparator.
           */
          private void initComparator()
          {
             if(comparator == null)
             {
                comparator = (tRowObject, tRowObject2) ->
                {
                   Object str1 = valueGetter.apply(tRowObject);
                   Object str2 = valueGetter.apply(tRowObject2);

                   if (htmlCell)
                   {
                      str1 = ((SafeHtml) str1).asString();
                      str2 = ((SafeHtml) str2).asString();
                   }
                   else
                   {
                      str1 = (String) str1;
                      str2 = (String) str2;

                   }
                   //noinspection NonJREEmulationClassesInClientCode
                   return ((String) str1).compareTo((String) str2);
                };
             }
          }
       }

       /**
        * A resource adapter used to override some of the grid's css styles.
        */
       private static class ResourcesAdapter
       implements DataGrid.Resources
       {
          /** The default resources */
          private final DataGrid.Resources resources;

          /** The style adapter */
          private final StyleAdapter style;

          /**
           * Ctor.
           *
           * @param   resources
           *          The default resources.
           */
          public ResourcesAdapter(final DataGrid.Resources resources) {
             this.resources = resources;
             this.style = new StyleAdapter();
          }

          @Override
          public ImageResource dataGridLoading() {
             return resources.dataGridLoading();
          }

          @Override
          public ImageResource dataGridSortAscending() {
             return resources.dataGridSortAscending();
          }

          @Override
          public ImageResource dataGridSortDescending() {
             return resources.dataGridSortDescending();
          }

          @Override
          public DataGrid.Style dataGridStyle() {
             return style;
          }
       }

       /**
        * The class provides custom css styles.
        */
       private static class StyleAdapter
       implements DataGrid.Style
       {
          /** boostrap styles prefix */
          private static final String B = "gwtb3-";

          /** an empty/dummy style */
          private static final String DUMMY = B + "d";

          @Override
          public boolean ensureInjected() {
             return true;
          }

          @Override
          public String getText() {
             return B;
          }

          @Override
          public String getName() {
             return B;
          }

          @Override
          public String dataGridCell() {
             return B + "cell";
          }

          @Override
          public String dataGridEvenRow() {
             return "even";
          }

          @Override
          public String dataGridEvenRowCell() {
             return DUMMY;
          }

          @Override
          public String dataGridFirstColumn() {
             return DUMMY; // Bootstrap3 uses "smart selectors"
          }

          @Override
          public String dataGridFirstColumnFooter() {
             return DUMMY;
          }

          @Override
          public String dataGridFirstColumnHeader() {
             return DUMMY;
          }

          @Override
          public String dataGridFooter() {
             return DUMMY;
          }

          @Override
          public String dataGridHeader() {
             return DUMMY;
          }

          @Override
          public String dataGridHoveredRow() {
             return "active";
          }

          @Override
          public String dataGridHoveredRowCell() {
             return "active";
          }

          @Override
          public String dataGridKeyboardSelectedCell() {
             return DUMMY;
          }

          @Override
          public String dataGridKeyboardSelectedRow() {
             return "key-active";
          }

          @Override
          public String dataGridKeyboardSelectedRowCell() {
             return "key-active";
          }

          @Override
          public String dataGridLastColumn() {
             return DUMMY;
          }

          @Override
          public String dataGridLastColumnFooter() {
             return DUMMY;
          }

          @Override
          public String dataGridLastColumnHeader() {
             return DUMMY;
          }

          @Override
          public String dataGridOddRow() {
             return "odd";
          }

          @Override
          public String dataGridOddRowCell() {
             return DUMMY;
          }

          @Override
          public String dataGridSelectedRow() {
             return "info";
          }

          @Override
          public String dataGridSelectedRowCell() {
             return DUMMY;
          }

          @Override
          public String dataGridSortableHeader() {
             return DUMMY;
          }

          @Override
          public String dataGridSortedHeaderAscending() {
             return DUMMY;
          }

          @Override
          public String dataGridSortedHeaderDescending() {
             return DUMMY;
          }

          @Override
          public String dataGridWidget() {
             return "table";
          }
       }
    }