Package com.lassitercg.faces.components.sheet

Source Code of com.lassitercg.faces.components.sheet.Sheet$RowColIndex

/*
* The MIT License (MIT)
* Copyright (c) 2013 Lassiter Consulting Group, LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.lassitercg.faces.components.sheet;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.el.ELContext;
import javax.el.ValueExpression;
import javax.faces.application.FacesMessage;
import javax.faces.application.ResourceDependencies;
import javax.faces.application.ResourceDependency;
import javax.faces.component.EditableValueHolder;
import javax.faces.component.FacesComponent;
import javax.faces.component.UIComponent;
import javax.faces.component.UIInput;
import javax.faces.component.UINamingContainer;
import javax.faces.component.behavior.ClientBehaviorHolder;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.primefaces.component.api.Widget;
import org.primefaces.context.RequestContext;
import org.primefaces.model.BeanPropertyComparator;
import org.primefaces.model.SortOrder;
import org.primefaces.util.ComponentUtils;

import com.lassitercg.faces.components.event.SheetUpdate;
import com.lassitercg.faces.components.util.VarBuilder;

/**
* Spreadsheet component wrappering the Handsontable jQuery UI component.
* <p>
* @author <a href="mailto:mlassiter@lassitercg.com">Mark Lassiter</a>
* @version $Id:$
*/
@FacesComponent(value = Sheet.COMPONENTTYPE)
@ResourceDependencies({ @ResourceDependency(name = "handsontable.js", target = "head", library = "handsontable"),
    @ResourceDependency(name = "sheet.js", target = "head", library = "handsontable"),
    @ResourceDependency(name = "handsontable.css", target = "head", library = "handsontable"), })
public class Sheet extends UIInput implements ClientBehaviorHolder, EditableValueHolder, Widget {

  public static final String EVENT_CELL_SELECT = "cellSelect";
  public static final String EVENT_CHANGE = "change";
  public static final String FAMILY = "com.lassitercg.faces.components";
  public static final String RENDERERTYPE = "com.lassitercg.faces.components.sheet";
  public static final String COMPONENTTYPE = "com.lassitercg.faces.components.sheet";
  public static final String PARTIAL_SOURCE_PARAM = "javax.faces.source";
  public static final String PARTIAL_BEHAVIOR_EVENT_PARAM = "javax.faces.behavior.event";

  /**
   * Properties that are tracked by state saving.
   */
  enum PropertyKeys {
    /**
     * <p>
     * The local value of this {@link UIComponent}.
     * </p>
     */
    value,

    /**
     * <p>
     * Flag indicating whether or not this component is valid.
     * </p>
     */
    valid,

    /**
     * <p>
     * The request scope attribute under which the data object for the
     * current row will be exposed when iterating.
     * </p>
     */
    var,

    /**
     * The selected row
     */
    selectedRow,

    /**
     * The last selected row
     */
    selectedLastRow,

    /**
     * The selected column
     */
    selectedColumn,

    /**
     * The last selected column
     */
    selectedLastColumn,
    /**
     * flag indication whether or not to show column headers
     */
    showColumnHeaders,

    /**
     * flag indication whether or not to show row headers
     */
    showRowHeaders,

    /**
     * Fixed rows when scrolling
     */
    fixedRows,

    /**
     * Fixed columns when scrolling
     */
    fixedCols,

    /**
     * The width of the component in pixels
     */
    width,

    /**
     * The height of the component in pixels
     */
    height,

    /**
     * The global error message to be displayed when the sheet is in error
     */
    errorMessage,

    /**
     * User style class for sheet
     */
    styleClass,

    /**
     * The style class to apply to the currently selected row
     */
    currentRowClass,

    /**
     * The style class to apply to the currently selected column
     */
    currentColClass,

    /**
     * The row key, used to unqiuely identify each row for update operations
     */
    rowKey,

    /**
     * The current sortBy value expression
     */
    sortBy,

    /**
     * The current direction of the sort
     */
    sortOrder,

    /**
     * The original sortBy value expression saved off for reset
     */
    origSortBy,

    /**
     * The original sort direction saved off for reset
     */
    origSortOrder,

    /**
     * The Handsontable stretchH value
     */
    stretchH,

    /**
     * The style class to apply to each row in the sheet (EL expression)
     */
    rowStyleClass,

    /**
     * The message displayed when no records are found
     */
    emptyMessage
  }

  /**
   * The list of UI Columns
   */
  private List<Column> columns;

  /**
   * List of bad updates
   */
  private List<BadUpdate> badUpdates;

  /**
   * The sorted list of data
   */
  private List<Object> sortedList;

  /**
   * Map of submitted values by row index and column index
   */
  private Map<RowColIndex, String> submittedValues = new HashMap<RowColIndex, String>();

  /**
   * Map of local values by row index and column index
   */
  private Map<RowColIndex, Object> localValues = new HashMap<RowColIndex, Object>();

  /**
   * Current row Index for iteration operations
   */
  private int rowIndex = -1;

  /**
   * The selection data
   */
  private String selection;

  /**
   * The id of the focused filter input if any
   */
  private String focusId;

  /**
   * Transient list of sheet updates that can be accessed after a successful
   * model update.
   */
  private final List<SheetUpdate> updates = new ArrayList<SheetUpdate>();

  /**
   * Maps a visible, rendered column index to the actual column based on
   * whether or not the column is rendered. Updated on encode, and used on
   * decode. Saved in the component state.
   */
  private Map<Integer, Integer> columnMapping;

  /**
   * Map by row keys for values found in list
   */
  private Map<Object, RowMap> rowMap;

  /*
   * (non-Javadoc)
   *
   * @see javax.faces.component.UIComponent#getFamily()
   */
  @Override
  public String getFamily() {
    return FAMILY;
  }

  /*
   * (non-Javadoc)
   *
   * @see javax.faces.component.UIComponentBase#getRendererType()
   */
  @Override
  public String getRendererType() {
    return RENDERERTYPE;
  }

  /*
   * (non-Javadoc)
   *
   * @see javax.faces.component.UIComponentBase#getEventNames()
   */
  @Override
  public Collection<String> getEventNames() {
    return Arrays.asList(EVENT_CHANGE, EVENT_CELL_SELECT);
  }

  /*
   * (non-Javadoc)
   *
   * @see javax.faces.component.UIComponentBase#getDefaultEventName()
   */
  @Override
  public String getDefaultEventName() {
    return EVENT_CHANGE;
  }

  /**
   * Update's the user's custom style class to be added to the div container
   * for the sheet.
   * <p>
   * @param styleClass
   */
  public void setStyleClass(String styleClass) {
    getStateHelper().put(PropertyKeys.styleClass, styleClass);
  }

  /**
   * The user's custom style class to be added to the div container for the
   * sheet.
   * <p>
   * @param styleClass
   */
  public String getStyleClass() {
    Object result = getStateHelper().eval(PropertyKeys.styleClass, null);
    if (result == null)
      return null;
    return result.toString();
  }

  /**
   * Update the stretcH value for the component
   * <p>
   * @param value
   */
  public void setStretchH(String value) {
    getStateHelper().put(PropertyKeys.stretchH, value);
  }

  /**
   * The handsontable stretchH value.
   * <p>
   * @return the stretchH value
   */
  public String getStretchH() {
    Object result = getStateHelper().eval(PropertyKeys.stretchH, null);
    if (result == null)
      return null;
    return result.toString();
  }

  /**
   * Update the emptyMessage value for the component
   * <p>
   * @param value
   */
  public void setEmptyMessage(String value) {
    getStateHelper().put(PropertyKeys.emptyMessage, value);
  }

  /**
   * The emptyMessage value.
   * <p>
   * @return the emptyMessage value
   */
  public String getEmptyMessage() {
    Object result = getStateHelper().eval(PropertyKeys.emptyMessage, null);
    if (result == null)
      return null;
    return result.toString();
  }

  /**
   * Update the current row style class
   * <p>
   * @param styleClass
   */
  public void setCurrentColClass(String styleClass) {
    getStateHelper().put(PropertyKeys.currentColClass, styleClass);
  }

  /**
   * The col style class to use for the selected col
   * <p>
   * @param styleClass
   */
  public String getCurrentColClass() {
    Object result = getStateHelper().eval(PropertyKeys.currentColClass, null);
    if (result == null)
      return null;
    return result.toString();
  }

  /**
   * Update the current row style class
   * <p>
   * @param styleClass
   */
  public void setCurrentRowClass(String styleClass) {
    getStateHelper().put(PropertyKeys.currentRowClass, styleClass);
  }

  /**
   * The row style class to use for the selected row
   * <p>
   * @param styleClass
   */
  public String getCurrentRowClass() {
    Object result = getStateHelper().eval(PropertyKeys.currentRowClass, null);
    if (result == null)
      return null;
    return result.toString();
  }

  /**
   * Update the current row style class to apply to the row
   * <p>
   * @param styleClass
   */
  public void setRowStyleClass(String styleClass) {
    getStateHelper().put(PropertyKeys.rowStyleClass, styleClass);
  }

  /**
   * The row style class to apply to each row
   * <p>
   * @param styleClass
   */
  public String getRowStyleClass() {
    Object result = getStateHelper().eval(PropertyKeys.rowStyleClass, null);
    if (result == null)
      return null;
    return result.toString();
  }

  /**
   * Update the ShowColumnheaders
   * <p>
   * @param value
   */
  public void setShowColumnHeaders(Boolean value) {
    getStateHelper().put(PropertyKeys.showColumnHeaders, value);
  }

  /**
   * Flag indicating whether or not column headers are visible
   * <p>
   * @return
   */
  public Boolean isShowColumnHeaders() {
    return Boolean.valueOf(getStateHelper().eval(PropertyKeys.showColumnHeaders, true).toString());
  }

  /**
   * Update the ShowRowHeaders value.
   * <p>
   * @param value
   */
  public void setShowRowHeaders(Boolean value) {
    getStateHelper().put(PropertyKeys.showRowHeaders, value);
  }

  /**
   * The ShowRowHeaders flag
   * <p>
   * @return
   */
  public Boolean isShowRowHeaders() {
    return Boolean.valueOf(getStateHelper().eval(PropertyKeys.showRowHeaders, true).toString());
  }

  /**
   * The list of child columns.
   * <p>
   * @return
   */
  public List<Column> getColumns() {
    if (columns == null) {
      columns = new ArrayList<Column>();
      getColumns(this);
    }
    return columns;
  }

  /**
   * Grabs the UIColumn children for the parent specified.
   * @param parent
   */
  private void getColumns(UIComponent parent) {
    for (UIComponent child : parent.getChildren())
      if (child instanceof Column)
        columns.add((Column) child);
  }

  /**
   * Updates the list of child columns.
   * <p>
   * @param columns
   */
  public void setColumns(List<Column> columns) {
    this.columns = columns;
  }

  /**
   * Updates the fixed row count.
   * <p>
   * @param value
   */
  public void setFixedRows(Integer value) {
    getStateHelper().put(PropertyKeys.fixedRows, value);
  }

  /**
   * The fixed row count
   * @return
   */
  public Integer getFixedRows() {
    Object result = getStateHelper().eval(PropertyKeys.fixedRows, null);
    if (result == null)
      return null;
    return Integer.valueOf(result.toString());
  }

  /**
   * Updates the fixed columns count.
   * <p>
   * @param value
   */
  public void setFixedCols(Integer value) {
    getStateHelper().put(PropertyKeys.fixedCols, value);
  }

  /**
   * The fixed column count.
   * <p>
   * @return
   */
  public Integer getFixedCols() {
    Object result = getStateHelper().eval(PropertyKeys.fixedCols, null);
    if (result == null)
      return null;
    return Integer.valueOf(result.toString());
  }

  /**
   * The list of bad updates
   * @return
   */
  public List<BadUpdate> getBadUpdates() {
    if (badUpdates == null)
      badUpdates = new ArrayList<BadUpdate>();
    return badUpdates;
  }

  /**
   * Resets the submitted values
   */
  public void resetSubmitted() {
    this.submittedValues.clear();
  }

  /**
   * Resets the sorting to the originally specified values (if any)
   */
  public void resetSort() {
    ValueExpression origSortBy = (ValueExpression) getStateHelper().get(PropertyKeys.origSortBy);
    if (origSortBy != null)
      this.setSortByValueExpression(origSortBy);

    String origSortOrder = (String) getStateHelper().get(PropertyKeys.origSortOrder);
    if (origSortOrder != null)
      setSortOrder(origSortOrder);
  }

  /**
   * Resets all filters, sorting and submitted values.
   */
  public void reset() {
    resetSubmitted();
    resetSort();
    localValues.clear();
    getBadUpdates().clear();
    for (Column c : getColumns())
      c.setFilterValue(null);
  }

  /**
   * Updates a submitted value.
   * <p>
   * @param row
   * @param col
   * @param value
   */
  public void setSubmittedValue(FacesContext context, int row, int col, String value) {
    // need to find row key
    this.setRowIndex(context, row);
    submittedValues.put(new RowColIndex(this.getRowKeyValue(context), col), value);
  }

  /**
   * Retrieves the submitted value for the row and col.
   * <p>
   * @param row
   * @param col
   * @return
   */
  public String getSubmittedValue(Object rowKey, int col) {
    return submittedValues.get(new RowColIndex(rowKey, col));
  }

  /**
   * Updates a local value.
   * <p>
   * @param rowKey
   * @param col
   * @param value
   */
  public void setLocalValue(Object rowKey, int col, Object value) {
    localValues.put(new RowColIndex(rowKey, col), value);
  }

  /**
   * Retrieves the submitted value for the rowKey and col.
   * <p>
   * @param row
   * @param col
   * @return
   */
  public Object getLocalValue(Object rowKey, int col) {
    return localValues.get(new RowColIndex(rowKey, col));
  }

  /**
   * The current row index for iterations over the List
   * @return
   */
  public int getRowIndex() {
    return rowIndex;
  }

  /**
   * Updates the row index for iterations over the list. The var value will be
   * update
   * <p>
   * @param context
   *            the FacesContext against which to the row var is set. Passed
   *            for performance
   * @param rowIndex
   */
  public void setRowIndex(FacesContext context, int rowIndex) {
    if (this.rowIndex != rowIndex) {
      this.rowIndex = rowIndex;

      if (context == null)
        return;

      if (rowIndex < 0) {
        context.getExternalContext().getRequestMap().remove(getVar());
      } else {
        final List<Object> values = this.getSortedValues();
        if (values == null)
          return;

        Object value = null;
        if (rowIndex < values.size())
          value = values.get(rowIndex);
        context.getExternalContext().getRequestMap().put(getVar(), value);
      }
    }
  }

  /**
   * Gets the object value of the row and col specified. If a local value
   * exists, that is returned, otherwise the actual value is return.
   * <p>
   * @param context
   * @param rowKey
   * @param col
   * @return
   */
  public Object getValueForCell(FacesContext context, Object rowKey, int col) {
    // if we have a local value, use it
    // note: can't check for null, as null may be the submitted value
    RowColIndex index = new RowColIndex(rowKey, col);
    if (localValues.containsKey(index))
      return localValues.get(index);

    RowMap map = rowMap.get(rowKey);
    setRowIndex(context, map.sortedIndex);
    final Column column = getColumns().get(col);
    return column.getValueExpression("value").getValue(context.getELContext());
  }

  /**
   * Gets the render string for the value the given cell. Applys the available
   * converters to convert the value.
   * <p>
   * @param context
   * @param rowKey
   * @param col
   * @return
   */
  public String getRenderValueForCell(FacesContext context, Object rowKey, int col) {

    // if we have a submitted value still, use it
    // note: can't check for null, as null may be the submitted value
    RowColIndex index = new RowColIndex(rowKey, col);
    if (submittedValues.containsKey(index))
      return submittedValues.get(index);

    Object value = getValueForCell(context, rowKey, col);
    if (value == null)
      return null;

    final Column column = getColumns().get(col);
    Converter converter = ComponentUtils.getConverter(context, column);
    if (converter == null)
      return value.toString();
    else
      return converter.getAsString(context, this, value);
  }

  /**
   * The currently selected column.
   * <p>
   * @return
   */
  public Integer getSelectedColumn() {
    Object result = getStateHelper().eval(PropertyKeys.selectedColumn);
    if (result == null)
      return null;
    return Integer.valueOf(result.toString());
  }

  /**
   * Updates the selected column.
   * <p>
   * @param col
   */
  public void setSelectedColumn(Integer col) {
    getStateHelper().put(PropertyKeys.selectedColumn, col);
  }

  /**
   * The currently selected column.
   * <p>
   * @return
   */
  public Integer getSelectedLastColumn() {
    Object result = getStateHelper().eval(PropertyKeys.selectedLastColumn);
    if (result == null)
      return null;
    return Integer.valueOf(result.toString());
  }

  /**
   * Updates the selected column.
   * <p>
   * @param col
   */
  public void setSelectedLastColumn(Integer col) {
    getStateHelper().put(PropertyKeys.selectedLastColumn, col);
  }

  /**
   * The currently selected row.
   * <p>
   * @return
   */
  public Integer getSelectedRow() {
    Object result = getStateHelper().eval(PropertyKeys.selectedRow);
    if (result == null)
      return null;
    return Integer.valueOf(result.toString());
  }

  /**
   * The currently selected row.
   * <p>
   * @return
   */
  public Integer getSelectedLastRow() {
    Object result = getStateHelper().eval(PropertyKeys.selectedLastRow);
    if (result == null)
      return null;
    return Integer.valueOf(result.toString());
  }

  /**
   * Updates the selected row.
   * <p>
   * @param row
   */
  public void setSelectedRow(Integer row) {
    getStateHelper().put(PropertyKeys.selectedRow, row);
  }

  /**
   * Updates the selected row.
   * <p>
   * @param row
   */
  public void setSelectedLastRow(Integer row) {
    getStateHelper().put(PropertyKeys.selectedLastRow, row);
  }

  /**
   * The width of the sheet in pixels
   * <p>
   * @return
   */
  public Integer getWidth() {
    Object result = getStateHelper().eval(PropertyKeys.width);
    if (result == null)
      return null;
    // this will handle any type so long as its convertable to integer
    return Integer.valueOf(result.toString());
  }

  /**
   * Updates the width
   * <p>
   * @param row
   */
  public void setWidth(Integer value) {
    getStateHelper().put(PropertyKeys.width, value);
  }

  /**
   * The height of the sheet. Note this is applied to the inner div which is
   * why it is recommend you use this property instead of a style class.
   * <p>
   * @return
   */
  public Integer getHeight() {
    Object result = getStateHelper().eval(PropertyKeys.height);
    if (result == null)
      return null;
    // this will handle any type so long as its convertable to integer
    return Integer.valueOf(result.toString());
  }

  /**
   * Updates the height
   * <p>
   * @param row
   */
  public void setHeight(Integer value) {
    getStateHelper().put(PropertyKeys.height, value);
  }

  /**
   * <p>
   * Return the value of the Sheet. This value must be a java.util.List value
   * at this time.
   * </p>
   */
  @Override
  public Object getValue() {
    return getStateHelper().eval(PropertyKeys.value);
  }

  /**
   * The sorted list of values.
   * <p>
   * @return
   */
  public List<Object> getSortedValues() {
    if (sortedList == null)
      sortAndFilter();
    return sortedList;
  }

  /**
   * Gets the rendered col index of the column corresponding to the current
   * sortBy. This is used to keep track of the current sort column in the
   * page.
   * <p>
   * @return
   */
  public int getSortColRenderIndex() {
    ValueExpression veSortBy = getValueExpression(PropertyKeys.sortBy.name());
    if (veSortBy == null)
      return -1;

    final String sortByExp = veSortBy.getExpressionString();
    int colIdx = 0;
    for (Column column : getColumns()) {
      if (!column.isRendered())
        continue;

      ValueExpression veCol = column.getValueExpression(PropertyKeys.sortBy.name());
      if (veCol != null) {
        if (veCol.getExpressionString().equals(sortByExp))
          return colIdx;
      }
      colIdx++;
    }
    return -1;
  }

  /**
   * Evaluates the specified item value against the column filters and if they
   * match, returns true, otherwise false.
   * <p>
   * @param obj
   * @return
   */
  protected boolean matchesFilter(Object obj) {
    for (Column col : getColumns()) {
      String filterValue = col.getFilterValue();
      if (StringUtils.isEmpty(filterValue))
        continue;

      Object filterBy = col.getFilterBy();
      // if we have a filter, but no value in the row, no match
      if (filterBy == null)
        return false;

      // case-insensitive
      String compareA = filterBy.toString().toLowerCase();
      String compareB = filterValue.toLowerCase();

      // TODO need to support match modes
      if (!compareA.contains(compareB))
        return false;
    }
    return true;
  }

  /**
   * Sorts and filters the data
   */
  @SuppressWarnings("unchecked")
  public void sortAndFilter() {
    sortedList = new ArrayList<Object>();
    rowMap = new HashMap<Object, RowMap>();

    Collection<?> values = (Collection<?>) getValue();
    if (values == null || values.isEmpty())
      return;

    boolean filters = false;
    for (Column col : getColumns())
      if (StringUtils.isNotEmpty(col.getFilterValue())) {
        filters = true;
        break;
      }

    FacesContext context = FacesContext.getCurrentInstance();
    Map<String, Object> requestMap = context.getExternalContext().getRequestMap();

    if (filters) {
      // iterate and add those matching the filters
      String var = getVar();
      for (Object obj : values) {
        requestMap.put(var, obj);
        try {
          if (matchesFilter(obj))
            sortedList.add(obj);
        } finally {
          requestMap.remove(var);
        }
      }
    } else
      sortedList.addAll(values);

    ValueExpression veSortBy = this.getValueExpression(PropertyKeys.sortBy.name());
    if (veSortBy != null)
      Collections.sort(sortedList, new BeanPropertyComparator(veSortBy, getVar(), convertSortOrder(), null));

    reMapRows();
  }

  /**
   * Remaps the row keys to the sorted and filtered list.
   */
  protected void reMapRows() {
    FacesContext context = FacesContext.getCurrentInstance();
    Map<String, Object> requestMap = context.getExternalContext().getRequestMap();

    for (int i = 0; i < sortedList.size(); i++) {
      Object obj = sortedList.get(i);
      String var = getVar();
      requestMap.put(var, obj);
      try {
        RowMap map = new RowMap();
        map.sortedIndex = i;
        map.value = obj;
        rowMap.put(getRowKeyValue(context), map);
      } finally {
        requestMap.remove(var);
      }
    }
  }

  /**
   * Gets the rowKey for the current row
   * <p>
   * @param context
   *            the faces context
   * @return a row key value or null if the expression is not set
   */
  protected Object getRowKeyValue(FacesContext context) {
    ValueExpression veRowKey = getValueExpression(PropertyKeys.rowKey.name());
    if (veRowKey == null)
      throw new RuntimeException("RowKey required on sheet!");
    Object value = veRowKey.getValue(context.getELContext());
    if (value == null)
      throw new RuntimeException("RowKey must resolve to non-null valkue for updates to work properly");
    return value;
  }

  /**
   * Convert to PF SortOrder enum since we are leveraging PF sorting code.
   * <p>
   * @return
   */
  protected SortOrder convertSortOrder() {
    String sortOrder = getSortOrder();
    if (sortOrder == null)
      return SortOrder.UNSORTED;
    else {
      SortOrder result = SortOrder.valueOf(sortOrder.toUpperCase(Locale.ENGLISH));
      return result;
    }
  }

  /**
   * <p>
   * Set the value of the <code>Sheet</code>. This value must be a
   * java.util.List at this time.
   * </p>
   * @param value
   *            the new value
   */
  @Override
  public void setValue(Object value) {
    getStateHelper().put(PropertyKeys.value, value);
  }

  /**
   * <p>
   * Return the request-scope attribute under which the data object for the
   * current row will be exposed when iterating. This property is
   * <strong>not</strong> enabled for value binding expressions.
   * </p>
   */
  public String getVar() {
    // must be a string literal (no eval)
    return (String) getStateHelper().get(PropertyKeys.var);
  }

  /**
   * <p>
   * Set the request-scope attribute under which the data object for the
   * current row wil be exposed when iterating.
   * </p>
   * @param var
   *            The new request-scope attribute name
   */
  public void setVar(String var) {
    getStateHelper().put(PropertyKeys.var, var);
  }

  /**
   * The current sortBy value expression in use.
   * @return
   */
  public ValueExpression getSortByValueExpression() {
    ValueExpression veSortBy = getValueExpression(PropertyKeys.sortBy.name());
    return veSortBy;
  }

  /**
   * Update the sort field
   * @param sortBy
   */
  public void setSortByValueExpression(ValueExpression sortBy) {
    // when updating, make sure we store off the original so it may be
    // restored
    ValueExpression orig = (ValueExpression) getStateHelper().get(PropertyKeys.origSortBy);
    if (orig == null) {
      getStateHelper().put(PropertyKeys.origSortBy, getSortByValueExpression());
    }
    setValueExpression(PropertyKeys.sortBy.name(), sortBy);
  }

  /**
   * The sort direction
   * @return
   */
  public String getSortOrder() {
    // if we have a toggled sort in our state, use it
    String result = (String) getStateHelper().eval(PropertyKeys.sortOrder, SortOrder.ASCENDING.toString());
    return result;
  }

  /**
   * Update the sort direction
   * @param sortOrder
   */
  public void setSortOrder(java.lang.String sortOrder) {
    // when updating, make sure we store off the original so it may be
    // restored
    String orig = (String) getStateHelper().get(PropertyKeys.origSortOrder);
    if (orig == null)
      // do not call getSortOrder as it defaults to ascending, we want
      // null
      // if this is the first call and there is no previous value.
      getStateHelper().put(PropertyKeys.origSortOrder, getStateHelper().eval(PropertyKeys.sortOrder));
    getStateHelper().put(PropertyKeys.sortOrder, sortOrder);
  }

  /**
   * The error message to display when the sheet is in error.
   * <p>
   * @return
   */
  public String getErrorMessage() {
    Object result = getStateHelper().eval(PropertyKeys.errorMessage);
    if (result == null)
      return null;
    return result.toString();
  }

  /**
   * Updates the errorMessage value.
   * @param value
   */
  public void setErrorMessage(String value) {
    getStateHelper().put(PropertyKeys.errorMessage, value);
  }

  /*
   * (non-Javadoc)
   *
   * @see javax.faces.component.UIInput#processValidators(javax.faces.context.
   * FacesContext)
   */
  @Override
  public void processValidators(FacesContext context) {
    super.processValidators(context);
  }

  /**
   * Converts each submitted value into a local value and stores it back in
   * the hash. If all values convert without error, then the component is
   * valid, and we can proceed to the processUpdates.
   */
  @Override
  public void validate(FacesContext context) {
    // iterate over submitted values and attempt to convert to the proper
    // data type. For successful values, remove from submitted and add to
    // local values map. for failures, add a conversion message and leave in
    // the submitted state
    Iterator<Entry<RowColIndex, String>> entries = submittedValues.entrySet().iterator();
    boolean hadBadUpdates = !getBadUpdates().isEmpty();
    getBadUpdates().clear();
    while (entries.hasNext()) {
      final Entry<RowColIndex, String> entry = entries.next();
      final Column column = getColumns().get(entry.getKey().colIndex);
      final String newValue = entry.getValue();
      final Object rowKey = entry.getKey().getRowKey();
      final int col = entry.getKey().getColIndex();
      final RowMap map = rowMap.get(rowKey);
      this.setRowIndex(context, map.sortedIndex);

      // attempt to convert new value from string to correct object type
      // based on column converter. Use PF util as helper
      Converter converter = ComponentUtils.getConverter(context, column);

      // assume string value if converter not found
      Object newValueObj = newValue;
      if (converter != null)
        try {
          newValueObj = converter.getAsObject(context, this, newValue);
        } catch (ConverterException e) {
          // add offending cell to list of bad updates
          // and to a stringbuffer for error messages (so we have one
          // message for the component)
          setValid(false);
          FacesMessage message = e.getFacesMessage();
          if (message == null) {
            message = new FacesMessage(FacesMessage.SEVERITY_ERROR, e.getMessage(), e.getMessage());
          }
          context.addMessage(this.getClientId(context), message);

          String messageText = message.getDetail();
          this.getBadUpdates()
              .add(new BadUpdate(getRowKeyValue(context), col, column, newValue, messageText));
          continue;
        }
      // value is fine, no further validations (again, not to be confused
      // with validators. until we have a "required" or something like
      // that, nothing else to do).
      setLocalValue(rowKey, col, newValueObj);

      // process validators on column
      column.setValue(newValueObj);
      try {
        column.validate(context);
      } finally {
        column.resetValue();
      }

      entries.remove();
    }
    this.setRowIndex(context, -1);

    final boolean newBadUpdates = !getBadUpdates().isEmpty();
    String errorMessage = this.getErrorMessage();

    if (hadBadUpdates || newBadUpdates) {
      // update the bad data var if partial request
      if (context.getPartialViewContext().isPartialRequest()) {
        this.sortAndFilter();
        this.renderBadUpdateScript(context);
      }
    }

    if (newBadUpdates && errorMessage != null) {
      FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, errorMessage, errorMessage);
      context.addMessage(null, message);
    }
  }

  /**
   * Override to update model with local values. Note that this is where
   * things can be fragile in that we can successfully update some values and
   * fail on others. There is no clean way to roll back the updates, but we
   * also need to fail processing.
   * <p>
   * TODO consider keeping old values as we update (need for event anyhow) and
   * if there is a failure attempt to roll back by updating successful model
   * updates with the old value. This may not all be necessary.
   */
  @Override
  public void updateModel(FacesContext context) {
    Iterator<Entry<RowColIndex, Object>> entries = localValues.entrySet().iterator();
    // Keep track of the dirtied rows for ajax callbacks so we can send
    // updates on what was touched
    HashSet<Object> dirtyRows = new HashSet<Object>();
    while (entries.hasNext()) {
      final Entry<RowColIndex, Object> entry = entries.next();

      final Object newValue = entry.getValue();
      final Object rowKey = entry.getKey().getRowKey();
      final int col = entry.getKey().getColIndex();
      final Column column = getColumns().get(col);
      final RowMap map = rowMap.get(rowKey);
      this.setRowIndex(context, map.sortedIndex);

      System.out.println("Local key=" + rowKey + " and sortedRow is " + map.sortedIndex);

      ValueExpression ve = column.getValueExpression(PropertyKeys.value.name());
      ELContext elContext = context.getELContext();
      Object oldValue = ve.getValue(elContext);
      ve.setValue(elContext, newValue);
      entries.remove();
      appendUpdateEvent(map.sortedIndex, col, map.value, oldValue, newValue);
      dirtyRows.add(rowKey);
    }
    setLocalValueSet(false);
    setRowIndex(context, -1);

    this.sortAndFilter();

    if (context.getPartialViewContext().isPartialRequest())
      this.renderRowUpdateScript(context, dirtyRows);
  }

  /**
   * Saves the state of the submitted and local values and the bad updates.
   */
  @Override
  public Object saveState(FacesContext context) {
    Object values[] = new Object[7];
    values[0] = super.saveState(context);
    values[1] = submittedValues;
    values[2] = localValues;
    values[3] = badUpdates;
    values[4] = columnMapping;
    values[5] = sortedList;
    values[6] = rowMap;

    return values;
  }

  /**
   * Restores the state for the submitted, local and bad values.
   */
  @SuppressWarnings("unchecked")
  @Override
  public void restoreState(FacesContext context, Object state) {
    if (state == null)
      return;

    Object values[] = (Object[]) state;
    super.restoreState(context, values[0]);
    Object restoredSubmittedValues = values[1];
    Object restoredLocalValues = values[2];
    Object restoredBadUpdates = values[3];
    Object restoredColMappings = values[4];
    Object restoredSortedList = values[5];
    Object restoredRowMap = values[6];

    if (restoredSubmittedValues == null)
      submittedValues.clear();
    else
      submittedValues = (Map<RowColIndex, String>) restoredSubmittedValues;

    if (restoredLocalValues == null)
      localValues.clear();
    else
      localValues = (Map<RowColIndex, Object>) restoredLocalValues;

    if (restoredBadUpdates == null)
      badUpdates.clear();
    else
      badUpdates = (List<BadUpdate>) restoredBadUpdates;

    if (restoredColMappings == null)
      columnMapping = null;
    else
      columnMapping = (Map<Integer, Integer>) restoredColMappings;

    if (restoredSortedList == null)
      sortedList = null;
    else
      sortedList = (List<Object>) restoredSortedList;

    if (restoredRowMap == null)
      rowMap = null;
    else
      rowMap = (Map<Object, RowMap>) restoredRowMap;
  }

  /**
   * The selection value.
   * <p>
   * @return the selection
   */
  public String getSelection() {
    return selection;
  }

  /**
   * Updates the selection value.
   * <p>
   * @param selection
   *            the selection to set
   */
  public void setSelection(String selection) {
    this.selection = selection;
  }

  /*
   * (non-Javadoc)
   *
   * @see javax.faces.component.EditableValueHolder#getSubmittedValue()
   */
  @Override
  public Object getSubmittedValue() {
    if (this.submittedValues.isEmpty())
      return null;
    else
      return (this.submittedValues);
  }

  /*
   * (non-Javadoc)
   *
   * @see
   * javax.faces.component.EditableValueHolder#setSubmittedValue(java.lang
   * .Object)
   */
  @SuppressWarnings("unchecked")
  @Override
  public void setSubmittedValue(Object submittedValue) {
    if (submittedValue == null)
      submittedValues.clear();
    else
      submittedValues = (Map<RowColIndex, String>) submittedValue;

  }

  /**
   * A list of updates from the last submission or ajax event.
   * <p>
   * @return the editEvent
   */
  public List<SheetUpdate> getUpdates() {
    return updates;
  }

  /**
   * Appends an update event
   * <p>
   * @param rowIndex
   * @param colIndex
   * @param rowData
   * @param oldValue
   * @param newValue
   */
  protected void appendUpdateEvent(int rowIndex, int colIndex, Object rowData, Object oldValue, Object newValue) {
    updates.add(new SheetUpdate(rowIndex, colIndex, rowData, oldValue, newValue));
  }

  /**
   * Returns true if any of the columns contain conditional styling.
   * <p>
   * @return
   */
  public boolean isHasStyledCells() {
    for (Column column : getColumns())
      if (column.getStyleClass() != null)
        return true;
    return false;
  }

  /**
   * Maps the rendered column index to the real column index.
   * <p>
   * @param renderIdx
   *            the rendered index
   * @return the mapped index
   */
  public int getMappedColumn(int renderIdx) {
    if (columnMapping == null) {
      return renderIdx;
    } else {
      Integer result = columnMapping.get(renderIdx);
      if (result == null)
        throw new IllegalArgumentException("Invalid index " + renderIdx);
      return result;
    }
  }

  /**
   * Provides the render column index based on the real index
   * @param realIdx
   * @return
   */
  public int getRenderIndexFromRealIdx(int realIdx) {
    if (columnMapping == null) {
      return realIdx;
    }

    for (Entry<Integer, Integer> entry : columnMapping.entrySet())
      if (entry.getValue().equals(realIdx))
        return entry.getKey();

    return realIdx;
  }

  /**
   * Updates the column mappings based on the rendered attribute
   */
  public void updateColumnMappings() {
    columnMapping = new HashMap<Integer, Integer>();
    int realIdx = 0;
    int renderIdx = 0;
    for (Column column : getColumns()) {
      if (column.isRendered()) {
        columnMapping.put(renderIdx, realIdx);
        renderIdx++;
      }
      realIdx++;
    }
  }

  /**
   * The number of rows in the value list.
   * <p>
   * @return
   */
  public int getRowCount() {
    List<Object> values = getSortedValues();
    if (values == null)
      return 0;
    return values.size();
  }

  /**
   * The focusId value.
   * <p>
   * @return the focusId
   */
  public String getFocusId() {
    return focusId;
  }

  /**
   * Updates the focusId value.
   * <p>
   * @param focusId
   *            the focusId to set
   */
  public void setFocusId(String focusId) {
    this.focusId = focusId;
  }

  /**
   * Invoke this method to commit the most recent set of ajax updates and
   * restart the tracking of changes. Use this when you have processes the
   * updates to the model and are confident that any changes made to this
   * point can be cleared (likely because you have persisted those changes).
   */
  public void commitUpdates() {
    resetSubmitted();
    FacesContext context = FacesContext.getCurrentInstance();
    if (context.getPartialViewContext().isPartialRequest()) {
      StringBuffer eval = new StringBuffer();
      String jQueryId = this.getClientId().replace(":", "\\\\:");
      String jsDeltaVar = this.getClientId().replace(":", "_") + "_delta";

      eval.append("$('#");
      eval.append(jQueryId);
      eval.append("_input').val('');");
      eval.append(jsDeltaVar);
      eval.append("={};");
      RequestContext.getCurrentInstance().getScriptsToExecute().add(eval.toString());
    }

  }

  /**
   * Generates the bad data var value for this sheet.
   * <p>
   * @param sheet
   * @param badDataVar
   * @return
   */
  public String getBadDataValue() {
    VarBuilder vb = new VarBuilder(null, true);
    for (BadUpdate badUpdate : getBadUpdates()) {
      final Object rowKey = badUpdate.getBadRowKey();
      final int col = getRenderIndexFromRealIdx(badUpdate.getBadColIndex());
      RowMap map = rowMap.get(rowKey);
      System.out.println("RowMap is " + map.sortedIndex + " for key " + rowKey);
      vb.appendRowColProperty(map.sortedIndex, col, badUpdate.getBadMessage().replace("'", "&apos;"), true);
    }
    return vb.closeVar().toString();
  }

  /**
   * Adds eval scripts to the ajax response to update the rows dirtied by the
   * most recent successful update request.
   * <p>
   * @param context
   *            the FacesContext
   * @param dirtyRows
   *            the set of dirty rows
   */
  protected void renderRowUpdateScript(FacesContext context, Set<Object> dirtyRows) {
    String jsVar = this.resolveWidgetVar();
    StringBuilder eval = new StringBuilder();

    for (Object rowKey : dirtyRows) {
      RowMap map = this.rowMap.get(rowKey);
      setRowIndex(context, map.sortedIndex);
      // data is array of array of data
      VarBuilder vbRow = new VarBuilder(null, false);
      for (int col = 0; col < getColumns().size(); col++) {
        final Column column = getColumns().get(col);
        if (!column.isRendered())
          continue;

        // render data value
        String value = getRenderValueForCell(context, rowKey, col);
        vbRow.appendArrayValue(value, true);
      }
      eval.append(jsVar);
      eval.append(".cfg.data[");
      eval.append(Integer.toString(map.sortedIndex));
      eval.append("]=");
      eval.append(vbRow.closeVar().toString());
      eval.append(";");
    }
    eval.append(jsVar);
    eval.append(".ht.render();");
    RequestContext.getCurrentInstance().getScriptsToExecute().add(eval.toString());
  }

  /**
   * Adds eval scripts to update the bad data array in the sheet to render
   * valdiation failures produced by the most recent ajax update attempt.
   * <p>
   * @param context
   *            the FacesContext
   */
  protected void renderBadUpdateScript(FacesContext context) {
    String widgetVar = this.resolveWidgetVar();
    String badDataVar = this.getBadDataValue();
    StringBuffer sb = new StringBuffer(widgetVar);
    sb.append(".cfg.errors=");
    sb.append(badDataVar);
    sb.append(";");
    sb.append(widgetVar);
    sb.append(".ht.render();");
    RequestContext.getCurrentInstance().getScriptsToExecute().add(sb.toString());

    sb = new StringBuffer();
    sb.append(widgetVar);
    sb.append(".sheetDiv.removeClass('ui-state-error')");
    if (!getBadUpdates().isEmpty())
      sb.append(".addClass('ui-state-error')");
    RequestContext.getCurrentInstance().getScriptsToExecute().add(sb.toString());
  }

  /*
   * (non-Javadoc)
   *
   * @see org.primefaces.component.api.Widget#resolveWidgetVar()
   */
  @Override
  public String resolveWidgetVar() {
    FacesContext context = FacesContext.getCurrentInstance();
    String userWidgetVar = (String) getAttributes().get("widgetVar");
    if (userWidgetVar != null)
      return userWidgetVar;
    else
      return "widget_" + getClientId(context).replaceAll("-|" + UINamingContainer.getSeparatorChar(context), "_");
  }

  /*
   * Private class used as a key for row,col maps.
   */
  private class RowColIndex implements Serializable {

    private static final long serialVersionUID = 1L;

    private final Object rowKey;
    private final Integer colIndex;

    /**
     * Constructs an instance of RowColIndex for the row and column
     * specified.
     * <p>
     * @param row
     *            the row represented by this index
     * @param col
     *            the column respresented by this index
     */
    public RowColIndex(Object rowKey, Integer col) {
      this.rowKey = rowKey;
      this.colIndex = col;
    }

    /*
     * (non-Javadoc)
     *
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(final Object other) {
      if (!(other instanceof RowColIndex))
        return false;
      RowColIndex castOther = (RowColIndex) other;
      return new EqualsBuilder().append(rowKey, castOther.rowKey).append(colIndex, castOther.colIndex)
          .isEquals();
    }

    /*
     * (non-Javadoc)
     *
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
      return new HashCodeBuilder().append(rowKey).append(colIndex).toHashCode();
    }

    /**
     * The rowIndex value.
     * <p>
     * @return the rowIndex
     */
    public Object getRowKey() {
      return rowKey;
    }

    /**
     * The colIndex value.
     * <p>
     * @return the colIndex
     */
    public Integer getColIndex() {
      return colIndex;
    }

  }

  /*
   * Private class used to map a row key to its object and sorted row index
   */
  private class RowMap implements Serializable {
    private static final long serialVersionUID = 1L;
    Object value;
    int sortedIndex;
  }
}
TOP

Related Classes of com.lassitercg.faces.components.sheet.Sheet$RowColIndex

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.