org.openfaces.component.table.TableDataModel.java Source code

Java tutorial

Introduction

Here is the source code for org.openfaces.component.table.TableDataModel.java

Source

/*
 * OpenFaces - JSF Component Library 2.0
 * Copyright (C) 2007-2010, TeamDev Ltd.
 * licensing@openfaces.org
 * Unless agreed in writing the contents of this file are subject to
 * the GNU Lesser General Public License Version 2.1 (the "LGPL" License).
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * Please visit http://openfaces.org/licensing/ for more details.
 */
package org.openfaces.component.table;

import org.apache.commons.collections.Predicate;
import org.openfaces.component.filter.AndFilterCriterion;
import org.openfaces.component.filter.Filter;
import org.openfaces.component.filter.FilterCriterion;
import org.openfaces.component.filter.PredicateBuilder;
import org.openfaces.util.DataUtil;
import org.openfaces.util.ValueBindings;

import javax.el.ValueExpression;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.model.DataModel;
import javax.faces.model.DataModelEvent;
import javax.faces.model.DataModelListener;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;

/**
 * @author Dmitry Pikhulya
 */

public class TableDataModel extends DataModel implements DataModelListener, Externalizable {
    private static final String VAR_FILTER_CRITERIA = "filterCriteria";
    private static final String VAR_PAGE_START = "pageStart";
    private static final String VAR_PAGE_SIZE = "pageSize";
    private static final String VAR_SORT_COLUMN_ID = "sortColumnId";
    private static final String VAR_SORT_COLUMN_INDEX = "sortColumnIndex";
    private static final String VAR_SORT_ASCENDING = "sortAscending";

    private Object wrappedData;
    /**
     * Should be used for iterating over rows if myExtractedRows is null.
     */
    private DataModel sourceDataModel;
    /**
     * If this field is non-null then mySourceDataModel shouldn't be used and myExtractedRows should be used instead.
     */
    private List<RowInfo> extractedRows;
    private List<Object> extractedRowKeys;
    private List<Object> allRetrievedRowKeys;
    private int extractedRowIndex = -1;
    private List<SortingRule> sortingRules;
    private List<Filter> filters;
    private int pageSize;
    private int pageIndex;
    private AbstractTable table;
    private ValueExpression rowKeyExpression;
    private ValueExpression rowDataByKeyExpression;

    private boolean internalIteration;
    private List<RowInfo> allRetrievedRows;
    private List<boolean[]> allRetrievedRowFilteringFlags;
    private List<Filter> currentlyAppliedFilters;
    private Integer totalRowCount;
    private int updateInProgress;
    private List<Object> previousRowKeys;

    public TableDataModel() {
        setWrappedData(null);
    }

    public TableDataModel(AbstractTable table) {
        this.table = table;
        setWrappedData(null);
    }

    public ValueExpression getRowKeyExpression() {
        return rowKeyExpression;
    }

    public void setRowKeyExpression(ValueExpression rowKeyExpression) {
        this.rowKeyExpression = rowKeyExpression;
    }

    public ValueExpression getRowDataByKeyExpression() {
        return rowDataByKeyExpression;
    }

    public void setRowDataByKeyExpression(ValueExpression rowDataByKeyBinding) {
        rowDataByKeyExpression = rowDataByKeyBinding;
    }

    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        sortingRules = (List<SortingRule>) in.readObject();
        rowKeyExpression = ValueBindings.readValueExpression(in);
        rowDataByKeyExpression = ValueBindings.readValueExpression(in);
        pageSize = in.readInt();
        pageIndex = in.readInt();
        setWrappedData(null);

        // restoring old extracted row keys is needed for correct restoreRows/restoreRowIndexes functionality, which
        // in turn is required for correct data submission in case of concurrent data modifications
        extractedRowKeys = (List) in.readObject();
    }

    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(sortingRules);
        ValueBindings.writeValueExpression(out, rowKeyExpression);
        ValueBindings.writeValueExpression(out, rowDataByKeyExpression);
        out.writeInt(pageSize);
        out.writeInt(pageIndex);
        out.writeObject(extractedRowKeys);
    }

    public Object getWrappedData() {
        return wrappedData;
    }

    public AbstractTable getTable() {
        return table;
    }

    public void setTable(AbstractTable table) {
        this.table = table;
    }

    public void setWrappedData(Object wrappedData) {
        this.wrappedData = wrappedData;
        DataModel dataModel = (wrappedData instanceof ValueExpression)
                ? new ValueExpressionDataModel((ValueExpression) wrappedData)
                : DataUtil.objectAsDataModel(wrappedData);
        setSourceDataModel(dataModel);
    }

    protected DataModel getSourceDataModel() {
        return sourceDataModel;
    }

    protected void setSourceDataModel(DataModel sourceDataModel) {
        if (this.sourceDataModel == sourceDataModel)
            return;
        if (this.sourceDataModel != null)
            this.sourceDataModel.removeDataModelListener(this);
        this.sourceDataModel = sourceDataModel;
        if (this.sourceDataModel != null)
            this.sourceDataModel.addDataModelListener(this);

        updateExtractedRows();
    }

    public boolean isRowAvailable() {
        if (extractedRows != null) {
            boolean rowIndexInRange = extractedRowIndex >= 0 && extractedRowIndex < extractedRows.size();
            return rowIndexInRange;
        }
        return sourceDataModel.isRowAvailable();
    }

    public int getRowCount() {
        if (extractedRows != null)
            return extractedRows.size();
        return sourceDataModel.getRowCount();
    }

    public Object getRowData() {
        if (extractedRows != null) {
            boolean rowIndexInRange = extractedRowIndex >= 0 && extractedRowIndex < extractedRows.size();
            if (rowIndexInRange) {
                RowInfo rowInfo = extractedRows.get(extractedRowIndex);
                return rowInfo != null ? rowInfo.getRowData() : null;
            } else
                throw new IllegalArgumentException(
                        "No row data is available for the current index: " + extractedRowIndex);
        }
        return sourceDataModel.getRowData();
    }

    public int getRowIndex() {
        if (extractedRows != null)
            return extractedRowIndex;
        return sourceDataModel.getRowIndex();
    }

    public Object getRowKey() {
        if (extractedRows != null) {
            boolean rowIndexInRange = extractedRowIndex >= 0 && extractedRowIndex < extractedRows.size();
            if (rowIndexInRange)
                return extractedRowKeys.get(extractedRowIndex);
            else
                throw new IllegalArgumentException(
                        "No row is available at the current index: " + extractedRowIndex);
        }
        FacesContext facesContext = FacesContext.getCurrentInstance();
        Map<String, Object> requestMap = facesContext.getExternalContext().getRequestMap();
        Object rowData = getRowData();
        Object result = requestRowKeyByRowData(facesContext, requestMap, table.getVar(), rowData, getRowIndex(),
                -1);
        return result;
    }

    public void setRowKey(Object rowKey) {
        int rowIndex = getRowIndexByRowKey(rowKey);
        setRowIndex(rowIndex);
    }

    public void setRowData(Object rowData) {
        int rowIndex = getRowIndexByRowData(rowData);
        setRowIndex(rowIndex);
    }

    public void setRowIndex(int rowIndex) {
        if (rowIndex < -1)
            throw new IllegalArgumentException("rowIndex shouldn't be less than -1: " + rowIndex);
        if (extractedRows != null) {
            if (extractedRowIndex == rowIndex)
                return;
            extractedRowIndex = rowIndex;
            boolean rowIndexInRange = extractedRowIndex >= 0 && extractedRowIndex < extractedRows.size();
            if (rowIndexInRange) {
                RowInfo rowInfo = extractedRows.get(rowIndex);
                fireRowSelected(rowIndex, rowInfo != null ? rowInfo.getRowData() : null);
                sourceDataModel.setRowIndex(rowInfo != null ? rowInfo.getIndexInOriginalList() : rowIndex);
            }
            return;
        }
        sourceDataModel.setRowIndex(rowIndex);
    }

    public void rowSelected(DataModelEvent dataModelEvent) {
        if (dataModelEvent.getDataModel() == sourceDataModel)
            originalDataModelRowSelected(dataModelEvent.getRowIndex(), dataModelEvent.getRowData());

    }

    private void fireRowSelected(int rowIndex, Object rowData) {
        DataModelListener[] dataModelListeners = getDataModelListeners();
        if (dataModelListeners != null) {
            DataModelEvent event = new DataModelEvent(this, rowIndex, rowData);
            for (DataModelListener dataModelListener : dataModelListeners) {
                dataModelListener.rowSelected(event);
            }
        }
    }

    private void originalDataModelRowSelected(int rowIndex, Object rowData) {
        if (internalIteration)
            return;
        fireRowSelected(rowIndex, rowData);
    }

    public int getPageSize() {
        return pageSize;
    }

    public void setPageSize(int pageSize) {
        if (pageSize < 0)
            throw new IllegalArgumentException("pageSize can't be less than zero: " + pageSize);
        if (this.pageSize == pageSize)
            return;
        this.pageSize = pageSize;
        updateExtractedRows();
    }

    public int getPageIndex() {
        return pageIndex;
    }

    public void setPageIndex(int pageIndex) {
        if (pageIndex < 0)
            throw new IllegalArgumentException("pageIndex can't be less than zero: " + pageIndex);
        if (updateInProgress == 0)
            pageIndex = validatePageIndex(pageIndex);
        if (this.pageIndex == pageIndex)
            return;
        this.pageIndex = pageIndex;
        if (getPageSize() != 0)
            updateExtractedRows();
    }

    private int validatePageIndex(int pageIndex) {
        int pageCount = getPageCount();
        if (pageCount != -1 && pageIndex >= pageCount)
            pageIndex = pageCount - 1;
        return pageIndex;
    }

    public List<SortingRule> getSortingRules() {
        return sortingRules;
    }

    public void setSortingRules(List<SortingRule> sortingRules) {
        this.sortingRules = sortingRules;
        updateExtractedRows();
    }

    public List getFilters() {
        return filters;
    }

    public void setFilters(List<Filter> filters) {
        boolean oldFiltersSpecified = this.filters != null;
        this.filters = filters;
        boolean newFiltersSpecified = this.filters != null;
        if (!oldFiltersSpecified && !newFiltersSpecified)
            return;

        updateExtractedRows();
    }

    private void updateExtractedRows() {
        if (updateInProgress > 0)
            return;
        if (areExtractedRowsNeeded()) {
            extractRows();
            extractedRowKeys = extractRowKeys(extractedRows);
            allRetrievedRowKeys = extractRowKeys(allRetrievedRows);
            setRowIndex(0);
        } else {
            updateValueExpressionModel();
            totalRowCount = null;
            extractedRows = null;
            extractedRowKeys = null;

            allRetrievedRows = null;
            allRetrievedRowKeys = null;
            allRetrievedRowFilteringFlags = null;
            currentlyAppliedFilters = null;
            setRowIndex(0);
        }
    }

    private boolean areExtractedRowsNeeded() {
        return true;
        //    return (isSortingNeeded()) ||
        //            isPaginationNeeded() ||
        //            isFilteringNeeded();
    }

    private boolean isFilteringNeeded() {
        return filters != null;
    }

    private void extractRows() {
        totalRowCount = null;
        boolean sortingNeeded = isSortingNeeded();
        boolean filteringNeeded = isFilteringNeeded() && filters.size() > 0;
        boolean paginationNeeded = isPaginationNeeded();

        boolean dataAlreadySorted = prepareForRetrievingSortedData(sortingNeeded);
        boolean dataAlreadyFiltered = dataAlreadySorted && prepareForRetrievingFilteredData(filteringNeeded);
        boolean dataAlreadyPaged = dataAlreadyFiltered && prepareForRetrievingPagedData(paginationNeeded);

        List<RowInfo> rows = extractRowsFromSourceDataModel();

        resetPreparedParameters();

        if (!dataAlreadySorted) {
            if (sortingNeeded)
                sortRows(rows);
        }
        allRetrievedRows = new ArrayList<RowInfo>(rows);

        List filteredRows;
        if (!dataAlreadyFiltered) {
            if (filteringNeeded && filters.size() > 0) {
                allRetrievedRowFilteringFlags = new ArrayList<boolean[]>(rows.size());
                rows = filterRows(filters, rows, allRetrievedRowFilteringFlags);
            } else
                allRetrievedRowFilteringFlags = null;
        } else
            allRetrievedRowFilteringFlags = null;

        filteredRows = new ArrayList<RowInfo>(rows);
        currentlyAppliedFilters = filters != null ? new ArrayList<Filter>(filters)
                : Collections.<Filter>emptyList();

        if (totalRowCount == null)
            totalRowCount = filteredRows.size();
        if (!dataAlreadyPaged) {
            if (paginationNeeded) {
                rows = extractCurrentPageRows(rows);
            }
        }
        extractedRows = rows;
    }

    private void resetPreparedParameters() {
        restoreRequestVariable(VAR_PAGE_START);
        restoreRequestVariable(VAR_PAGE_SIZE);
        restoreRequestVariable(VAR_SORT_COLUMN_ID);
        restoreRequestVariable(VAR_SORT_COLUMN_INDEX);
        restoreRequestVariable(VAR_SORT_ASCENDING);
        restoreRequestVariable(VAR_FILTER_CRITERIA);
    }

    private boolean prepareForRetrievingSortedData(boolean sortingNeeded) {
        boolean customDataProvidingRequested = isCustomDataProvidingRequested();
        if (sortingNeeded && !customDataProvidingRequested)
            return false;

        if (customDataProvidingRequested) {
            setRequestVariable(VAR_SORT_COLUMN_ID, table.getSortColumnId());
            setRequestVariable(VAR_SORT_COLUMN_INDEX, table.getSortColumnIndex());
            setRequestVariable(VAR_SORT_ASCENDING, table.isSortAscending());
        }
        return true;
    }

    private boolean isCustomDataProvidingRequested() {
        if (table == null)
            return false;
        if (!(table instanceof DataTable))
            return false;
        return ((DataTable) table).getCustomDataProviding();
    }

    private boolean prepareForRetrievingFilteredData(boolean filteringNeeded) {
        boolean customDataProvidingRequested = isCustomDataProvidingRequested();
        if (!customDataProvidingRequested)
            return !filteringNeeded;

        setFilteringCriteriaToRequestVariable();
        return true;
    }

    private void setFilteringCriteriaToRequestVariable() {
        List<FilterCriterion> criteria = new ArrayList<FilterCriterion>();
        AndFilterCriterion andCriterion = new AndFilterCriterion(criteria);
        if (filters != null)
            for (Filter filter : filters) {
                FilterCriterion filterCriterion = (FilterCriterion) filter.getValue();
                if (filterCriterion == null || filterCriterion.acceptsAll())
                    continue;

                criteria.add(filterCriterion);
            }
        setRequestVariable(VAR_FILTER_CRITERIA, andCriterion);
    }

    private boolean prepareForRetrievingPagedData(boolean paginationNeeded) {
        if (!paginationNeeded)
            return true;
        boolean customDataProvidingRequested = isCustomDataProvidingRequested();
        if (!customDataProvidingRequested)
            return false;

        totalRowCount = requestNonPagedRowCount();
        int pageSize = getPageSize();
        int pageIndex = getPageIndex();
        int pageCount = getPageCount();
        if (pageIndex >= pageCount)
            pageIndex = pageCount - 1;
        int pageStart = pageIndex * pageSize;
        int remainingRows = totalRowCount - pageStart;
        int thisRangeSize = remainingRows < pageSize ? remainingRows : pageSize;

        setRequestVariable(VAR_PAGE_START, pageStart);
        setRequestVariable(VAR_PAGE_SIZE, thisRangeSize);
        return true;
    }

    private void setRequestVariable(String varName, Object varValue) {
        Map<String, Object> requestMap = getRequestMap();
        Object prevVarValue = requestMap.put(varName, varValue);
        String backupVarName = "of:prev_" + varName;
        Stack<Object> backupValues = (Stack<Object>) requestMap.get(backupVarName);
        if (backupValues == null) {
            backupValues = new Stack<Object>();
            requestMap.put(backupVarName, backupValues);
        }
        backupValues.push(prevVarValue);
    }

    private void restoreRequestVariable(String varName) {
        Map<String, Object> requestMap = getRequestMap();
        if (requestMap == null) {
            return;
        }
        String backupVarName = "of:prev_" + varName;
        Stack backupValues = (Stack) requestMap.get(backupVarName);
        if (backupValues == null || backupValues.isEmpty())
            return;
        Object oldValue = backupValues.pop();
        requestMap.put(varName, oldValue);
    }

    private int requestNonPagedRowCount() {
        AbstractTable table = getTable();
        setFilteringCriteriaToRequestVariable();
        ValueExpression valueExpression = table.getValueExpression("totalRowCount");
        if (valueExpression == null)
            throw new IllegalStateException(
                    "totalRowCount must be defined for pagination with custom data providing to work. table id = "
                            + table.getClientId(FacesContext.getCurrentInstance()));
        Object value = valueExpression.getValue(FacesContext.getCurrentInstance().getELContext());
        restoreRequestVariable(VAR_FILTER_CRITERIA);
        if (value.getClass().equals(Long.class) || value.getClass().equals(long.class))
            value = ((Long) value).intValue();
        if (!(value instanceof Integer))
            throw new IllegalStateException("totalRowCount must return an int (or Integer) number, but returned: "
                    + (value != null ? value.getClass().getName() : "null") + "; table id = "
                    + table.getClientId(FacesContext.getCurrentInstance()));
        return (Integer) value;
    }

    private Map<String, Object> getRequestMap() {
        FacesContext facesContext = FacesContext.getCurrentInstance();
        if (facesContext == null)
            return null;
        ExternalContext externalContext = facesContext.getExternalContext();
        return externalContext.getRequestMap();
    }

    private boolean isPaginationNeeded() {
        return getPageSize() > 0;
    }

    private boolean isSortingNeeded() {
        return sortingRules != null && sortingRules.size() > 0;
    }

    /**
     * @return list of RowInfo instances
     */
    private List<RowInfo> extractRowsFromSourceDataModel() {
        List<RowInfo> extractedRows;
        internalIteration = true;
        try {
            updateValueExpressionModel();
            int rowCount = sourceDataModel.getRowCount();
            if (rowCount == -1)
                rowCount = Integer.MAX_VALUE;
            extractedRows = new ArrayList<RowInfo>();
            for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) {
                sourceDataModel.setRowIndex(rowIndex);
                if (!sourceDataModel.isRowAvailable())
                    break;
                Object currentRowData = sourceDataModel.getRowData();
                if (currentRowData == null)
                    throw new NullPointerException(
                            "There must not be null rows in a DataTable/TreeTable. table id: "
                                    + getTable().getClientId(FacesContext.getCurrentInstance()));
                extractedRows.add(new RowInfo(currentRowData, rowIndex));
            }
        } finally {
            internalIteration = false;
        }
        return extractedRows;
    }

    private void updateValueExpressionModel() {
        if (sourceDataModel instanceof ValueExpressionDataModel)
            ((ValueExpressionDataModel) sourceDataModel).readData();
    }

    private static List<RowInfo> filterRows(List<Filter> filters, List<RowInfo> sortedRows,
            List<boolean[]> filteringFlags) {
        List<RowInfo> result = new ArrayList<RowInfo>();
        int sortedRowCount = sortedRows.size();
        for (int i = 0; i < sortedRowCount; i++) {
            RowInfo rowObj = sortedRows.get(i);

            boolean[] flagsArray = new boolean[filters.size()];
            boolean rowAccepted = filterRow(filters, rowObj, flagsArray);
            filteringFlags.add(flagsArray);
            if (rowAccepted)
                result.add(rowObj);
        }
        return result;
    }

    public static boolean filterRow(List<Filter> filters, Object rowObj, boolean[] flagsArray) {
        Object data = (rowObj instanceof RowInfo) ? ((RowInfo) rowObj).getRowData() // RowInfo for DataTable (for storing original row indexes)
                : rowObj; // row data object for TreeTable (for there's no notion of index in TreeTable)
        boolean rowAccepted = true;
        for (int filterIndex = 0, filterCount = filters.size(); filterIndex < filterCount; filterIndex++) {
            Filter filter = filters.get(filterIndex);
            FilterCriterion filterValue = (FilterCriterion) filter.getValue();

            Predicate predicate = filterValue != null ? PredicateBuilder.build(filterValue) : null;
            boolean filterAcceptsData = predicate == null || predicate.evaluate(data);
            if (!filterAcceptsData)
                rowAccepted = false;
            flagsArray[filterIndex] = filterAcceptsData;
        }

        return rowAccepted;
    }

    private List<RowInfo> extractCurrentPageRows(List<RowInfo> extractedRows) {
        int rowCount = extractedRows.size();
        if (rowCount == 0)
            extractedRows = Collections.emptyList();
        else {
            int pageSize = getPageSize();
            int pageIndex = getPageIndex();
            int fromIndex = pageIndex * pageSize;

            if (fromIndex >= rowCount)
                extractedRows = Collections.emptyList();
            else {
                int toIndex = fromIndex + pageSize;
                if (toIndex >= rowCount)
                    toIndex = rowCount;
                extractedRows = extractedRows.subList(fromIndex, toIndex);
            }
        }
        return extractedRows;
    }

    private List<Object> extractRowKeys(List<RowInfo> rows) {
        if (rows.size() == 0)
            return Collections.emptyList();
        FacesContext facesContext = FacesContext.getCurrentInstance();
        Map<String, Object> requestMap = facesContext.getExternalContext().getRequestMap();
        String var = table.getVar();
        int rowCount = rows.size();
        List<Object> extractedRowKeys = new ArrayList<Object>(rowCount);
        for (int i = 0; i < rowCount; i++) {
            RowInfo rowInfo = rows.get(i);
            Object rowData = rowInfo.getRowData();
            Object rowKey = requestRowKeyByRowData(facesContext, requestMap, var, rowData, i,
                    rowInfo.getIndexInOriginalList());
            extractedRowKeys.add(rowKey);
        }
        return extractedRowKeys;
    }

    private void sortRows(List<RowInfo> extractedRows) {
        if (table == null)
            return;
        final Comparator<Object> rowDataComparator = table.createRowDataComparator(sortingRules);
        Comparator<RowInfo> rowInfoComparator = new Comparator<RowInfo>() {
            public int compare(RowInfo rowInfo1, RowInfo rowInfo2) {
                return rowDataComparator.compare(rowInfo1.getRowData(), rowInfo2.getRowData());
            }
        };
        if (rowDataComparator != null)
            Collections.sort(extractedRows, rowInfoComparator);
    }

    public Object requestRowKeyByRowData(FacesContext facesContext, Map<String, Object> requestMap, String var,
            Object rowData, int rowIndex, int indexInOriginalList) {
        if (rowKeyExpression == null) {
            if (isValidRowKey(rowData))
                return rowData;
            else
                return (indexInOriginalList != -1) ? new DefaultRowKey(rowIndex, indexInOriginalList)
                        : new DefaultRowKey(rowIndex);
        }
        if (requestMap == null) {
            requestMap = facesContext.getExternalContext().getRequestMap();
        }
        if (var == null) {
            var = getTable().getVar();
        }
        Object prevVarValue = requestMap.put(var, rowData);
        Object result = rowKeyExpression.getValue(facesContext.getELContext());
        requestMap.put(var, prevVarValue);
        if (result == null)
            throw new RuntimeException("The rowKey binding \"" + rowKeyExpression.getExpressionString()
                    + "\" of table with client id \"" + getTable().getClientId(facesContext)
                    + "\" must return a non-null value\n");
        if (!isValidRowKey(result))
            throw new RuntimeException("Invalid value returned from rowKey binding \""
                    + rowKeyExpression.getExpressionString() + "\" of table with client id \""
                    + getTable().getClientId(facesContext) + "\"\n"
                    + "    It must return a value that implements java.io.Serializable interface and correctly implements the equals and hashCode methods for serialized instances. \n"
                    + "    An instance of the following class that doesn't satisfy these rules has been returned: "
                    + result.getClass().getName() + ", for this row data: " + rowData);

        return result;
    }

    private static Set<Class> approvedRowKeyClasses = new HashSet<Class>();

    public static boolean isValidRowKey(Object rowKey) {
        Class rowKeyClass = rowKey.getClass();
        if (approvedRowKeyClasses.contains(rowKeyClass))
            return true;
        boolean result = rowKey instanceof Serializable && checkSerializableEqualsAndHashcode(rowKey);
        if (result)
            approvedRowKeyClasses.add(rowKeyClass);
        return result;
    }

    private Object requestRowDataByRowKey(FacesContext facesContext, Object rowKey) {
        if (rowDataByKeyExpression == null)
            return null;
        Map<String, Object> requestMap = facesContext.getExternalContext().getRequestMap();
        requestMap.put("rowKey", rowKey);
        Object result = rowDataByKeyExpression.getValue(facesContext.getELContext());
        return result;
    }

    private int getRowIndexByRowKey(Object key) {
        if (key == null)
            return -1;
        if (key instanceof DefaultRowKey) {
            DefaultRowKey defaultRowKey = (DefaultRowKey) key;
            return defaultRowKey.getRowIndex();
        }
        if (extractedRows != null) {
            int index = extractedRowKeys.indexOf(key);
            return index;
        }
        int rowCount = getRowCount();
        if (rowCount == -1)
            rowCount = Integer.MAX_VALUE;
        for (int i = 0; i < rowCount; i++) {
            setRowIndex(i);
            if (!isRowAvailable())
                return -1;
            Object currentRowKey = getRowKey();
            if (key.equals(currentRowKey))
                return i;
        }
        return -1;
    }

    private int getRowIndexByRowData(Object data) {
        if (data == null)
            return -1;
        if (extractedRows != null) {
            for (int index = 0, extractedRowCount = extractedRows.size(); index < extractedRowCount; index++) {
                RowInfo rowInfo = extractedRows.get(index);
                Object rowData = rowInfo != null ? rowInfo.getRowData() : null;
                if (rowData != null && rowData.equals(data))
                    return index;
            }
            return -1;
        }
        int rowCount = getRowCount();
        if (rowCount == -1)
            rowCount = Integer.MAX_VALUE;
        for (int i = 0; i < rowCount; i++) {
            setRowIndex(i);
            if (!isRowAvailable())
                return -1;
            Object currentRowData = getRowData();
            if (data.equals(currentRowData))
                return i;
        }
        // todo: it appears that this method will fail in finding index by data if DataTable's rowKey attribute is defined, but there's no equals/hashCode for node data itself.
        // todo: check this and add search by row key for such situations
        return -1;
    }

    public int getPageCount() {
        int pageSize = getPageSize();
        if (pageSize == 0)
            return -1;
        int rowCount = getTotalRowCount();
        if (rowCount == -1)
            return -1;
        if (rowCount == 0)
            return 1;
        int pageCount = rowCount / pageSize;
        if (rowCount % pageSize > 0)
            pageCount++;
        return pageCount;
    }

    public int getTotalRowCount() {
        return (totalRowCount != null) ? totalRowCount : sourceDataModel.getRowCount();
    }

    public RowInfo getRowInfoByRowKey(Object key) {
        if (key == null)
            return null;
        if (allRetrievedRows != null) {
            int index = allRetrievedRowKeys.indexOf(key);
            if (index != -1)
                return allRetrievedRows.get(index);
        }
        int rowCount = getRowCount();
        if (rowCount == -1)
            rowCount = Integer.MAX_VALUE;
        for (int i = 0; i < rowCount; i++) {
            setRowIndex(i);
            if (!isRowAvailable())
                return null;
            Object currentRowKey = getRowKey();
            if (key.equals(currentRowKey))
                return new RowInfo(getRowData(), i);
        }
        Object rowData = requestRowDataByRowKey(FacesContext.getCurrentInstance(), key);
        return new RowInfo(rowData, -1);
    }

    public List<Object> getRowListForFiltering(Filter filter) {
        return getRowListForFiltering(filter, currentlyAppliedFilters, allRetrievedRows,
                allRetrievedRowFilteringFlags);
    }

    public static List<Object> getRowListForFiltering(Filter filter, List<Filter> lastFilteringFilters,
            List<?> allRows, List<boolean[]> allRowFilteringFlags) {
        if (lastFilteringFilters != null && lastFilteringFilters.size() > 0) {
            if (allRowFilteringFlags == null)
                return rowDatasFromRowInfos(allRows);
            int requestedFilterIndex = lastFilteringFilters.indexOf(filter);
            List<Object> result = new ArrayList<Object>();
            rowIteration: for (int rowIndex = 0, allRowCount = allRows.size(); rowIndex < allRowCount; rowIndex++) {
                Object rowObj = allRows.get(rowIndex);
                Object data = (rowObj instanceof RowInfo) ? ((RowInfo) rowObj).getRowData() // RowInfo for DataTable (for storing original row indexes)
                        : rowObj; // row data object for TreeTable (for there's no notion of index in TreeTable)
                boolean[] rowFlags = allRowFilteringFlags.get(rowIndex);
                for (int filterIndex = 0; filterIndex < rowFlags.length; filterIndex++) {
                    if (filterIndex == requestedFilterIndex)
                        continue;
                    boolean filterAcceptsRow = rowFlags[filterIndex];
                    if (!filterAcceptsRow)
                        continue rowIteration;
                }
                result.add(data);
            }
            return result;
        } else
            return rowDatasFromRowInfos(allRows);
    }

    private static List<Object> rowDatasFromRowInfos(List<?> allRows) {
        List<Object> result = new ArrayList<Object>(allRows.size());
        for (Object rowObj : allRows) {
            if (rowObj instanceof RowInfo) {
                RowInfo rowInfo = (RowInfo) rowObj;
                result.add(rowInfo.getRowData());
            } else
                result.add(rowObj);
        }
        return result;
    }

    public void startUpdate() {
        updateInProgress++;
    }

    public void endUpdate() {
        if (updateInProgress == 0)
            throw new IllegalStateException("endUpdate is called while the model is not in the update state");
        updateInProgress--;
        if (updateInProgress == 0) {
            updateExtractedRows();
            int pageIndex = getPageIndex();
            int newPageIndex = validatePageIndex(pageIndex);
            if (newPageIndex != pageIndex) {
                this.pageIndex = newPageIndex;
                updateExtractedRows();
            }
        }

    }

    public boolean isSourceDataModelEmpty() {
        DataModel sourceDataModel = getSourceDataModel();
        if (sourceDataModel == null)
            return true;
        int rowCount = sourceDataModel.getRowCount();
        return rowCount == 0;
    }

    private int getOldRowIndexByRowKey(Object key) {
        if (key == null)
            return -1;
        if (extractedRowKeys != null) {
            int index = extractedRowKeys.indexOf(key);
            if (index != -1)
                return index;
        }
        return -1;
    }

    public void setWrappedData(List rowDatas, List rowKeys) {
        extractedRows = new ArrayList<RowInfo>(rowDatas.size());
        for (Object rowData : rowDatas) {
            extractedRows.add(new RowInfo(rowData, -1));
        }
        extractedRowKeys = rowKeys;
    }

    public static class RestoredRowIndexes {
        private final int[] oldIndexes;
        private final Set<Integer> unavailableRowIndexes;

        public RestoredRowIndexes(int[] oldIndexes, Set<Integer> unavailableRowIndexes) {
            this.oldIndexes = oldIndexes;
            this.unavailableRowIndexes = unavailableRowIndexes;
        }

        public int[] getOldIndexes() {
            return oldIndexes;
        }

        public Set<Integer> getUnavailableRowIndexes() {
            return unavailableRowIndexes;
        }
    }

    /**
     * This method should be called before the fresh data has been read into the TableDataModel.
     * So this method should be called early in the request processing lifecycle, then should go the
     * data reading procedure, which updates myExtractedRows in TableDataModel, and then goes the call
     * to restoreRowIndexes() method or restoreRows() method.
     */
    public void prepareForRestoringRowIndexes() {
        previousRowKeys = new ArrayList<Object>(extractedRowKeys);
    }

    public List getStoredRowKeys() {
        return previousRowKeys;
    }

    public RestoredRowIndexes restoreRowIndexes() {
        List<Object> restoredRowKeys = previousRowKeys;
        if (restoredRowKeys == null)
            throw new IllegalStateException();

        Set<Integer> unavailableRowIndexes = new HashSet<Integer>();
        int restoredRowCount = restoredRowKeys.size();
        int[] oldRowIndexes = new int[restoredRowCount];
        List<RowInfo> restoredRowDatas = new ArrayList<RowInfo>(restoredRowCount);
        for (int i = 0; i < restoredRowCount; i++) {
            Object rowKey = restoredRowKeys.get(i);
            int oldRowIndex = getOldRowIndexByRowKey(rowKey);
            oldRowIndexes[i] = oldRowIndex;
            RowInfo rowInfo = oldRowIndex != -1 ? getRowInfoByRowKey(rowKey) : null;
            Object rowData = rowInfo != null ? rowInfo.getRowData() : null;
            if (rowData == null)
                unavailableRowIndexes.add(i);
            restoredRowDatas.add(new RowInfo(rowData, -1));
        }

        extractedRows = restoredRowDatas;
        extractedRowKeys = restoredRowKeys;
        return new RestoredRowIndexes(oldRowIndexes, unavailableRowIndexes);
    }

    public void addRows(int atIndex, List rowDatas, List<?> rowKeys) {
        for (int i = 0; i < rowDatas.size(); i++) {
            Object newRowData = rowDatas.get(i);
            extractedRows.add(atIndex + i, new RowInfo(newRowData, -1));
        }
        extractedRowKeys.addAll(atIndex, rowKeys);
    }

    public Set<Integer> restoreRows(boolean readActualData) {
        List<Object> restoredRowKeys = previousRowKeys;
        if (restoredRowKeys == null)
            throw new IllegalStateException();

        Set<Integer> unavailableRowIndexes = new HashSet<Integer>();
        int restoredRowCount = restoredRowKeys.size();
        List<RowInfo> restoredRowDatas = new ArrayList<RowInfo>(restoredRowCount);
        for (int i = 0; i < restoredRowCount; i++) {
            if (!readActualData) {
                unavailableRowIndexes.add(i);
                continue;
            }
            Object rowKey = restoredRowKeys.get(i);
            RowInfo rowInfo = getRowInfoByRowKey(rowKey);
            Object rowData = rowInfo != null ? rowInfo.getRowData() : null;
            if (rowData == null)
                unavailableRowIndexes.add(i);
            restoredRowDatas.add(rowInfo);
        }

        extractedRows = restoredRowDatas;
        extractedRowKeys = restoredRowKeys;
        return unavailableRowIndexes;
    }

    private static boolean checkSerializableEqualsAndHashcode(Object rowKey) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        Object deserializedRowKey;
        try {
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(rowKey);
            oos.close();
            byte[] serializedObject = baos.toByteArray();
            ByteArrayInputStream bais = new ByteArrayInputStream(serializedObject);
            ObjectInputStream ois = new ObjectInputStream(bais);
            deserializedRowKey = ois.readObject();
            bais.close();
        } catch (IOException e) {
            throw new RuntimeException(
                    "The rowData or rowKey object is marked as Serializable but can't be serialized: "
                            + rowKey.getClass().getName()
                            + " ; check that all object's fields are also Serializable",
                    e);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
        boolean equalsValid = deserializedRowKey.equals(rowKey);
        boolean hashCodeValid = deserializedRowKey.hashCode() == rowKey.hashCode();
        boolean result = equalsValid && hashCodeValid;
        return result;
    }

    public static class RowInfo {
        private final Object rowData;
        private final int indexInOriginalList;

        public RowInfo(Object rowData, int indexInOriginalList) {
            this.rowData = rowData;
            this.indexInOriginalList = indexInOriginalList;
        }

        public Object getRowData() {
            return rowData;
        }

        public int getIndexInOriginalList() {
            return indexInOriginalList;
        }
    }
}