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";
      }
   }
}