Java tutorial
/* * OpenFaces - JSF Component Library 3.0 * Copyright (C) 2007-2012, 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.impl; 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.component.table.*; import org.openfaces.util.Components; import org.openfaces.util.DataUtil; import org.openfaces.util.ValueBindings; import javax.el.ValueExpression; import javax.faces.FacesException; import javax.faces.component.StateHolder; 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.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.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * This class is only for internal usage from within the OpenFaces library. It shouldn't be used explicitly * by any application code. * * @author Dmitry Pikhulya */ public class TableDataModel extends DataModel implements DataModelListener, StateHolder { 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 Map<Object, ? extends NodeInfo> derivedRowHierarchy; private List<Object> extractedRowKeys; private List<Object> allRetrievedRowKeys; private int extractedRowIndex = -1; private List<GroupingRule> groupingRules; 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; private Boolean clearUnDisplayedSelection; 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 Object saveState(FacesContext context) { return new Object[] { groupingRules, sortingRules, rowKeyExpression, rowDataByKeyExpression, pageSize, pageIndex, extractedRowKeys }; } public void restoreState(FacesContext context, Object stateObj) { Object[] state = (Object[]) stateObj; int i = 0; groupingRules = (List<GroupingRule>) state[i++]; sortingRules = (List<SortingRule>) state[i++]; rowKeyExpression = (ValueExpression) state[i++]; rowDataByKeyExpression = (ValueExpression) state[i++]; pageSize = (Integer) state[i++]; pageIndex = (Integer) state[i++]; 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) state[i++]; } public boolean isTransient() { return false; } public void setTransient(boolean newTransientValue) { throw new UnsupportedOperationException(); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { groupingRules = (List<GroupingRule>) in.readObject(); 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(groupingRules); 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 getNodeLevel() { RowInfo rowInfo = getRowInfo(); return rowInfo != null ? rowInfo.getLevel() : 0; } public int getMaxNodeLevel() { if (extractedRows != null) { int maxNodeLevel = 0; for (RowInfo rowInfo : extractedRows) { if (maxNodeLevel < rowInfo.getLevel()) maxNodeLevel = rowInfo.getLevel(); if (maxNodeLevel > rowInfo.getLevel()) break; } return maxNodeLevel; } return 0; } public RowInfo getRowInfo() { if (extractedRows != null) { boolean rowIndexInRange = extractedRowIndex >= 0 && extractedRowIndex < extractedRows.size(); if (rowIndexInRange) { RowInfo rowInfo = extractedRows.get(extractedRowIndex); return rowInfo; } else throw new IllegalArgumentException( "No row info is available for the current index: " + extractedRowIndex); } return null; } 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<GroupingRule> getGroupingRules() { return groupingRules; } public void setGroupingRules(List<GroupingRule> groupingRules) { this.groupingRules = groupingRules; updateExtractedRows(); } 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; extractRows(); extractedRowKeys = extractRowKeys(extractedRows); allRetrievedRowKeys = extractRowKeys(allRetrievedRows); setRowIndex(0); } 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; if (groupingRules != null && groupingRules.size() > 0) { derivedRowHierarchy = groupRows(groupingRules, rows); } else { derivedRowHierarchy = 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; } /** * This method is only for internal usage from within the OpenFaces library. It shouldn't be used explicitly * by any application code. */ public List<RowInfo> getAllRetrievedRows() { return allRetrievedRows; } public Map<Object, ? extends NodeInfo> getDerivedRowHierarchy() { return derivedRowHierarchy; } /** * @param groupingRules a list of GroupingRule instances representing the requested grouping hierarchy * @param rows RowInfo objects representing the data rows that are already sorted according to the passed grouping * rules hierarchy * @param level hierarchy level of the rows passed in the rows parameters. The group rows created by this method * invocation (excluding the recursive invocations) will have this value of their level property. * @return a list of RowInfo instances representing the newly created group header rows for the top-level grouping * rule (the first one in the list), and having the group header rows for its lower-level grouping rules * in the immediateSubRows field of the appropriate top-level RowInfos, and so deeper into the hierarchy of * grouping rules. The "leaf" RowInfos (the ones stored in the immediateSubRows field of the deepest * hierarchy level represent the actual data rows and not group header rows. */ private List<RowInfo> constructGroupingTree(List<GroupingRule> groupingRules, List<RowInfo> rows, int level, RowGroup parentRowGroup) { if (groupingRules.size() == 0) return rows; int rowCount = rows.size(); if (rowCount == 0) return Collections.emptyList(); GroupingRule groupingRule = groupingRules.get(0); FacesContext context = FacesContext.getCurrentInstance(); RowComparator ruleComparator = table.createRuleComparator(context, groupingRule); String columnId = groupingRule.getColumnId(); ColumnGroupingInfo columnGroupingInfo = getColumnGroupingInfo(columnId); List<RowInfo> thisLevelGroupHeaderRowInfos = new ArrayList<RowInfo>(); Runnable exitColumnContext = columnGroupingInfo.enterColumnContext(); try { RowInfo currentGroupRowInfo = null; int subRowsLevel = level + 1; for (int i = 0; i < rowCount; i++) { RowInfo rowInfo = rows.get(i); RowInfo nextRowInfo = i < rowCount - 1 ? rows.get(i + 1) : null; if (currentGroupRowInfo == null) { currentGroupRowInfo = createHeaderRowInfo(context, columnGroupingInfo, rowInfo.getRowData(), level, parentRowGroup); currentGroupRowInfo.setAllDataRowsInThisGroup(new ArrayList<RowInfo>()); } rowInfo.setLevel(subRowsLevel); currentGroupRowInfo.getAllDataRowsInThisGroup().add(rowInfo); boolean lastRowInThisGroup = nextRowInfo == null || !recordsInTheSameGroup(ruleComparator, rowInfo, nextRowInfo); if (lastRowInThisGroup) { thisLevelGroupHeaderRowInfos.add(currentGroupRowInfo); GroupHeader groupHeader = (GroupHeader) currentGroupRowInfo.getRowData(); RowGroup rowGroup = groupHeader.getRowGroup(); List<RowInfo> subRowInfos = constructGroupingTree( groupingRules.subList(1, groupingRules.size()), currentGroupRowInfo.getAllDataRowsInThisGroup(), subRowsLevel, rowGroup); if (columnGroupingInfo.isInHeadersSpecified()) { subRowInfos.add(0, new RowInfo(new InGroupHeader(rowGroup), -1, subRowsLevel)); } if (columnGroupingInfo.isInGroupFootersSpecified()) { if (columnGroupingInfo.isInGroupFootersCollapsible()) subRowInfos.add(new RowInfo(new InGroupFooter(rowGroup), -1, subRowsLevel)); else thisLevelGroupHeaderRowInfos.add(new RowInfo(new InGroupFooter(rowGroup), -1, level)); } if (columnGroupingInfo.isGroupFooterSpecified()) { if (columnGroupingInfo.isGroupFootersCollapsible()) subRowInfos.add(new RowInfo(new GroupFooter(rowGroup), -1, subRowsLevel)); else thisLevelGroupHeaderRowInfos.add(new RowInfo(new GroupFooter(rowGroup), -1, level)); } currentGroupRowInfo.setImmediateSubRows(subRowInfos); currentGroupRowInfo = null; } } } finally { if (exitColumnContext != null) exitColumnContext.run(); } return thisLevelGroupHeaderRowInfos; } private void linearizeGroupingTree(List<RowInfo> topLevelRowInfos, List<RowInfo> targetRowList) { for (RowInfo rowInfo : topLevelRowInfos) { targetRowList.add(rowInfo); List<RowInfo> subRows = rowInfo.getImmediateSubRows(); if (subRows != null && subRows.size() > 0) linearizeGroupingTree(subRows, targetRowList); } } private Map<Object, ? extends NodeInfo> groupRows(List<GroupingRule> groupingRules, List<RowInfo> rows) { clearCachedColumnGroupingInfos(); List<RowInfo> topLevelRowInfos = constructGroupingTree(groupingRules, rows, 0, null); rows.clear(); linearizeGroupingTree(topLevelRowInfos, rows); Map<Object, NodeInfo> rowIndexToChildCount = new HashMap<Object, NodeInfo>(); rowIndexToChildCount.put("root", createNodeInfo(-1, topLevelRowInfos.size())); for (int rowIndex = 0, rowCount = rows.size(); rowIndex < rowCount; rowIndex++) { RowInfo rowInfo = rows.get(rowIndex); Object rowData = rowInfo.getRowData(); if (!(rowData instanceof GroupHeader)) continue; List<RowInfo> immediateSubRows = rowInfo.getImmediateSubRows(); rowIndexToChildCount.put(rowIndex, immediateSubRows != null ? createNodeInfo(rowInfo.getLevel(), immediateSubRows.size()) : createNodeInfo(rowInfo.getLevel(), rowInfo.getAllDataRowsInThisGroup().size())); } return rowIndexToChildCount; } private DataTableNodeInfo createNodeInfo(int nodeLevel, Integer childCount) { return new DataTableNodeInfo(nodeLevel, childCount, true); } private boolean recordsInTheSameGroup(Comparator<Object> comparator, RowInfo rowInfo1, RowInfo rowInfo2) { Object record1 = rowInfo1 != null ? rowInfo1.getRowData() : null; Object record2 = rowInfo2 != null ? rowInfo2.getRowData() : null; if (record1 == null || record2 == null) return false; if (record1 instanceof GroupHeaderOrFooter || record2 instanceof GroupHeaderOrFooter) return false; return comparator.compare(record1, record2) == 0; } private RowInfo createHeaderRowInfo(FacesContext context, ColumnGroupingInfo columnGroupingInfo, Object anyRowDataInThisGroup, int level, RowGroup parentRowGroup) { RowGroup currentGroup = createRowGroup(context, columnGroupingInfo, anyRowDataInThisGroup, parentRowGroup); GroupHeader groupHeader = new GroupHeader(currentGroup); return new RowInfo(groupHeader, -1, level); } private RowGroup createRowGroup(FacesContext context, ColumnGroupingInfo columnGroupingInfo, Object anyRowDataInThisGroup, RowGroup parentRowGroup) { Runnable restoreParams = table.populateSortingExpressionParams(table.getVar(), context.getExternalContext().getRequestMap(), anyRowDataInThisGroup); Object groupingValue; try { groupingValue = columnGroupingInfo.getColumnGroupingValueExpression().getValue(context.getELContext()); } finally { restoreParams.run(); } return new RowGroup(columnGroupingInfo.getColumnId(), groupingValue, parentRowGroup); } private void resetPreparedParameters() { Components.restoreRequestVariable(VAR_PAGE_START); Components.restoreRequestVariable(VAR_PAGE_SIZE); Components.restoreRequestVariable(VAR_SORT_COLUMN_ID); Components.restoreRequestVariable(VAR_SORT_COLUMN_INDEX); Components.restoreRequestVariable(VAR_SORT_ASCENDING); Components.restoreRequestVariable(VAR_FILTER_CRITERIA); } private boolean prepareForRetrievingSortedData(boolean sortingNeeded) { boolean customDataProvidingRequested = isCustomDataProvidingRequested(); if (sortingNeeded && !customDataProvidingRequested) return false; if (customDataProvidingRequested) { Components.setRequestVariable(VAR_SORT_COLUMN_ID, table.getSortColumnId()); Components.setRequestVariable(VAR_SORT_COLUMN_INDEX, table.getSortColumnIndex()); Components.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); } Components.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; Components.setRequestVariable(VAR_PAGE_START, pageStart); Components.setRequestVariable(VAR_PAGE_SIZE, thisRangeSize); return true; } private int requestNonPagedRowCount() { AbstractTable table = getTable(); setFilteringCriteriaToRequestVariable(); if (!table.getRowsDecodingRequired()) { return table.getTotalRowCount() == null ? 0 : table.getTotalRowCount(); } 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()); Components.restoreRequestVariable(VAR_FILTER_CRITERIA); 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 boolean isPaginationNeeded() { return getPageSize() > 0; } private boolean isSortingNeeded() { return (sortingRules != null && sortingRules.size() > 0) || (groupingRules != null && groupingRules.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>(); for (RowInfo rowObj : sortedRows) { 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(groupingRules, 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 (rowData instanceof GroupHeaderOrFooter) { return rowData; } 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 final Map<Class, Boolean> rowKeyClassesValidFlags = new HashMap<Class, Boolean>(); public static boolean isValidRowKey(Object rowKey) { Class rowKeyClass = rowKey.getClass(); synchronized (rowKeyClassesValidFlags) { Boolean rowKeyValid = rowKeyClassesValidFlags.get(rowKeyClass); if (rowKeyValid == null) { rowKeyValid = checkSerializableEqualsAndHashcode(rowKey); rowKeyClassesValidFlags.put(rowKeyClass, rowKeyValid); } return rowKeyValid; } } 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) { if (!(rowKey instanceof Serializable)) return false; 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; private int level; private List<RowInfo> immediateSubRows; private List<RowInfo> allDataRowsInThisGroup; private RowInfo parentGroup; public RowInfo(Object rowData, int indexInOriginalList) { this(rowData, indexInOriginalList, 0); } public RowInfo(Object rowData, int indexInOriginalList, int level) { this.rowData = rowData; this.indexInOriginalList = indexInOriginalList; this.level = level; } public void setLevel(int level) { this.level = level; } public RowInfo getParentGroup() { return parentGroup; } public void setParentGroup(RowInfo parentGroup) { this.parentGroup = parentGroup; } public Object getRowData() { return rowData; } public int getIndexInOriginalList() { return indexInOriginalList; } public int getLevel() { return level; } public List<RowInfo> getImmediateSubRows() { return immediateSubRows; } public void setImmediateSubRows(List<RowInfo> immediateSubRows) { for (RowInfo subRow : immediateSubRows) { subRow.setParentGroup(this); } this.immediateSubRows = immediateSubRows; } /** * @return a list of all data rows in this group and all of its sub-groups */ public List<RowInfo> getAllDataRowsInThisGroup() { return allDataRowsInThisGroup; } public void setAllDataRowsInThisGroup(List<RowInfo> allDataRowsInThisGroup) { this.allDataRowsInThisGroup = allDataRowsInThisGroup; } } private Map<String, ColumnGroupingInfo> columnGroupingInfos = new HashMap<String, ColumnGroupingInfo>(); /** * Invoking this once for data iteration is needed to stay up to date to the current RowGrouping settings cached * here. */ private void clearCachedColumnGroupingInfos() { columnGroupingInfos.clear(); } private ColumnGroupingInfo getColumnGroupingInfo(String columnId) { ColumnGroupingInfo columnGroupingInfo = columnGroupingInfos.get(columnId); if (columnGroupingInfo != null) return columnGroupingInfo; BaseColumn column = table.getColumnById(columnId); columnGroupingInfo = new ColumnGroupingInfo(column); columnGroupingInfos.put(columnId, columnGroupingInfo); return columnGroupingInfo; } public Boolean isObjectInList(Object rowData) { for (RowInfo extractedRow : extractedRows) { if (extractedRow.getRowData().equals(rowData)) return true; } return false; } /** * This class contains the pre-extracted column's data that is repeatedly required during the row grouping process, * to avoid having to retrieve this information each time it is needed. */ private class ColumnGroupingInfo { private BaseColumn column; private String columnId; private boolean inHeadersSpecified; private boolean inGroupFootersSpecified; private boolean groupFooterSpecified; private boolean inGroupFootersCollapsible; private boolean groupFootersCollapsible; private ValueExpression columnGroupingValueExpression; public ColumnGroupingInfo(BaseColumn column) { this.column = column; columnId = column.getId(); columnGroupingValueExpression = getColumnGroupingValueExpression(columnId); if (columnGroupingValueExpression == null) throw new FacesException("The column by which grouping is performed should have its " + "value, groupingExpression or sortingExpression attribute defined, or have a " + "UIOutputComponent from which the grouping expression can be derived automatically. " + "Column id: " + columnId); // The presence of per-column group headers and footers for a certain group depends not on the declaration // of the column whose data unites the records in this group, but on the presence of per-column group // header/footer facets in at least of one of the rendered columns DataTable table = (DataTable) column.getTable(); List<BaseColumn> renderedColumns = table.getRenderedColumns(); for (BaseColumn renderedColumn : renderedColumns) { inHeadersSpecified |= renderedColumn.getInGroupHeader() != null; inGroupFootersSpecified |= renderedColumn.getInGroupFooter() != null; if (inHeadersSpecified && inGroupFootersSpecified) break; } groupFooterSpecified = column.getGroupFooter() != null; RowGrouping rowGrouping = table.getRowGrouping(); inGroupFootersCollapsible = rowGrouping.getInGroupFootersCollapsible(); groupFootersCollapsible = rowGrouping.getGroupFootersCollapsible(); } private ValueExpression getColumnGroupingValueExpression(String columnId) { if (columnId == null) return null; List<BaseColumn> allColumns = table.getAllColumns(); BaseColumn baseColumn = table.findColumnById(allColumns, columnId); return baseColumn.getColumnGroupingExpression(); } /** * @return a Runnable instance which restores the context to the old state as it was prior to invoking this method */ public Runnable enterColumnContext() { if (column instanceof DynamicColumn) { return ((DynamicColumn) column).enterComponentContext(); } return null; } public String getColumnId() { return columnId; } public ValueExpression getColumnGroupingValueExpression() { return columnGroupingValueExpression; } public boolean isInHeadersSpecified() { return inHeadersSpecified; } public boolean isInGroupFootersSpecified() { return inGroupFootersSpecified; } public boolean isGroupFooterSpecified() { return groupFooterSpecified; } public boolean isInGroupFootersCollapsible() { return inGroupFootersCollapsible; } public boolean isGroupFootersCollapsible() { return groupFootersCollapsible; } } public Boolean getClearUnDisplayedSelection() { return clearUnDisplayedSelection; } public void setClearUnDisplayedSelection(Boolean clearUnDisplayedSelection) { this.clearUnDisplayedSelection = clearUnDisplayedSelection; } }