com.lassitercg.faces.components.sheet.Sheet.java Source code

Java tutorial

Introduction

Here is the source code for com.lassitercg.faces.components.sheet.Sheet.java

Source

/*
 * 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.collections.CollectionUtils;
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>();

    private Map<RowColIndex, String> comments = new HashMap<RowColIndex, String>();

    /**
     * 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));
    }

    public void setComment(FacesContext context, int row, int col, String comment) {
        this.setRowIndex(context, row);
        setComment(context, this.getRowKeyValue(context), col, comment);
    }

    private void setComment(FacesContext context, Object rowKey, int col, String value) {
        comments.put(new RowColIndex(rowKey, col), value);
        final Column column = getColumns().get(col);
        ValueExpression valueExpression = column.getValueExpression("comment");
        if (valueExpression != null) {
            valueExpression.setValue(context.getELContext(), value);
        }
    }

    public String getComment(FacesContext context, Object rowKey, int col) {
        RowColIndex index = new RowColIndex(rowKey, col);
        if (!comments.containsKey(index)) {
            final Column column = getColumns().get(col);
            ValueExpression valueExpression = column.getValueExpression("comment");
            return (valueExpression == null) ? null : (String) valueExpression.getValue(context.getELContext());
        }
        return comments.get(index);
    }

    /**
     * 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);
            }
        }
    }

    public String getCommentForCell(FacesContext context, Object rowKey, int col) {
        return getComment(context, rowKey, col);
    }

    /**
     * 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) {
        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) {
        HashSet<Object> dirtyRows = new HashSet<Object>();
        Iterator<Entry<RowColIndex, String>> commentsEntries = comments.entrySet().iterator();
        while (commentsEntries.hasNext()) {
            final Entry<RowColIndex, String> entry = commentsEntries.next();
            final String 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);

            String oldValue = null;
            ValueExpression valueExpression = column.getValueExpression("comment");
            if (valueExpression != null) {
                oldValue = (String) valueExpression.getValue(context.getELContext());
            }
            commentsEntries.remove();
            if (!StringUtils.equals(oldValue, newValue)) {
                appendUpdateEvent(map.sortedIndex, col, map.value, oldValue, newValue);
                dirtyRows.add(rowKey);
            }
        }

        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      
        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);
            if (!column.isReadonlyCell()) {
                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) {
            if (badUpdates != 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();
    }

    public void updateDirtyRows(Set<Object> dirtyRows) {
        FacesContext context = FacesContext.getCurrentInstance();
        renderRowUpdateScript(context, dirtyRows);
        setRowIndex(context, -1);
    }

    /**
     * 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;

                String value = getRenderValueForCell(context, rowKey, col);
                vbRow.appendArrayValue(value, true);
                String styleClass = column.getStyleClass();
                if (styleClass != null) {
                    eval.append(jsVar);
                    eval.append(".cfg.styles['r");
                    eval.append(rowIndex);
                    eval.append("_c");
                    eval.append(col);
                    eval.append("']='");
                    eval.append(styleClass);
                    eval.append("';");
                }
            }
            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(";");
        }

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