/******************************************************************************* * Copyright (c) 2014, 2015 Dirk Fauth and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Dirk Fauth <dirk.fauth@googlemail.com> - initial API and implementation * Dirk Fauth <dirk.fauth@googlemail.com> - Bug 459422 * Evan O'Connell <oconn.e@gmail.com> - Bug 460640 ******************************************************************************/ package org.eclipse.nebula.widgets.nattable.extension.glazedlists.groupBy; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.eclipse.nebula.widgets.nattable.config.DefaultComparator; import org.eclipse.nebula.widgets.nattable.data.IColumnAccessor; import org.eclipse.nebula.widgets.nattable.extension.glazedlists.groupBy.summary.IGroupBySummaryProvider; import org.eclipse.nebula.widgets.nattable.layer.IUniqueIndexLayer; import org.eclipse.nebula.widgets.nattable.layer.LabelStack; import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell; import org.eclipse.nebula.widgets.nattable.sort.ISortModel; import org.eclipse.nebula.widgets.nattable.sort.SortDirectionEnum; import org.eclipse.nebula.widgets.nattable.tree.TreeLayer; import ca.odell.glazedlists.TreeList; /** * {@link Comparator} that is used to sort the {@link TreeList} based on the * groupBy information. Necessary for building the tree structure correctly. * * @param <T> * The type of the base objects carried in the TreeList * * @see GroupByTreeFormat */ public class GroupByComparator<T> implements IGroupByComparator<T> { protected final GroupByModel groupByModel; protected final IColumnAccessor<T> columnAccessor; protected ISortModel sortModel; protected IUniqueIndexLayer treeLayer; private GroupByDataLayer<T> dataLayer; /** * Cache that is used to increase the performance on sorting. The * information whether a column is a summary column is only retrieved once * and not calculated everytime. */ protected Map<Integer, Boolean> summaryColumnCache = new HashMap<Integer, Boolean>(); /** * Cache that is used to increase the performance on sorting by summary * values. Necessary because the summary value is not carried by the * {@link GroupByObject} but retrieved from the {@link GroupByDataLayer}. * Since the retrieval is done via row index rather than the * {@link GroupByObject}, the row index needs to be calculated in order to * get the correct values. This is done via {@link List#indexOf(Object)} * which is quite time consuming. So this cache is used to reduce the amount * of indexOf calls drastically. * <p> * As this cache is only used to increase the performance on sorting, the * life time of this cache is reduced to a sort operation. It therefore gets * cleared on every structural change. * </p> */ protected Map<GroupByObject, GroupByObjectValueCache> groupByObjectComparatorCache = new HashMap<GroupByObject, GroupByObjectValueCache>(); /** * * @param groupByModel * The {@link GroupByModel} necessary to retrieve information * about the current groupBy state. * @param columnAccessor * The {@link IColumnAccessor} necessary to retrieve the column * values of elements. */ public GroupByComparator(GroupByModel groupByModel, IColumnAccessor<T> columnAccessor) { this.groupByModel = groupByModel; this.columnAccessor = columnAccessor; } /** * * @param groupByModel * The {@link GroupByModel} necessary to retrieve information * about the current groupBy state. * @param columnAccessor * The {@link IColumnAccessor} necessary to retrieve the column * values of elements. * @param dataLayer * The {@link GroupByDataLayer} that should be used to retrieve * groupBy summary values for sorting the tree structure. Can be * <code>null</code> to avoid retrieving and inspecting summary * values on sorting. */ public GroupByComparator(GroupByModel groupByModel, IColumnAccessor<T> columnAccessor, GroupByDataLayer<T> dataLayer) { this.groupByModel = groupByModel; this.columnAccessor = columnAccessor; this.dataLayer = dataLayer; } @SuppressWarnings({ "rawtypes", "unchecked" }) @Override public int compare(Object o1, Object o2) { if (o1 == null) { if (o2 == null) { return 0; } else { return -1; } } else if (o2 == null) { return 1; } else { int result = 0; for (int columnIndex : this.groupByModel.getGroupByColumnIndexes()) { if (o1 instanceof GroupByObject && o2 instanceof GroupByObject) { // handle GroupByObject comparison GroupByObject g1 = (GroupByObject) o1; GroupByObject g2 = (GroupByObject) o2; // get column index for groupBy value // we assume the descriptor is an ordered map, therefore we // choose the last element in the map int groupByIndex = columnIndex; for (Map.Entry<Integer, Object> entry : g1.getDescriptor().entrySet()) { groupByIndex = entry.getKey(); } Comparator comparator = getComparator(groupByIndex); result = g1.getDescriptor().size() - g2.getDescriptor().size(); if (result == 0) { if (this.sortModel != null && this.sortModel.getSortedColumnIndexes() != null && !this.sortModel.getSortedColumnIndexes().isEmpty()) { for (int sortColumnIndex : this.sortModel.getSortedColumnIndexes()) { Boolean summaryColumn = isSummaryColumn(g1, sortColumnIndex); if (summaryColumn == null) { // in case we were not able to retrieve the // information for the one GroupByObject we // try to find the information the other // GroupByObject summaryColumn = isSummaryColumn(g2, sortColumnIndex); } if (summaryColumn != null && summaryColumn) { // compare GroupByObjects by summary // value Object sumValue1 = getSummaryValueFromCache(g1, sortColumnIndex); Object sumValue2 = getSummaryValueFromCache(g2, sortColumnIndex); result = getComparator(sortColumnIndex).compare(sumValue1, sumValue2); } if (result == 0) { result = comparator.compare(g1.getValue(), g2.getValue()); } if ((isTreeColumn(sortColumnIndex) || (summaryColumn != null && summaryColumn)) && this.sortModel.getSortDirection(sortColumnIndex).equals(SortDirectionEnum.DESC)) { result = result * -1; } } } else { result = comparator.compare(g1.getValue(), g2.getValue()); } return result; } } else if (o1 instanceof GroupByObject && !(o2 instanceof GroupByObject)) { result = 1; } else if (!(o1 instanceof GroupByObject) && o2 instanceof GroupByObject) { result = -1; } else { // both values are not a GroupByObject so we need to sort by // value to ensure the correct ordering for the tree // structure Object value1 = this.columnAccessor.getDataValue((T) o1, columnIndex); Object value2 = this.columnAccessor.getDataValue((T) o2, columnIndex); result = getComparator(columnIndex).compare(value1, value2); } if (result != 0) { return result; } } } return 0; } /** * * @param columnIndex * The column index of the column that should be checked. * @return <code>true</code> if the column at the given index is the tree * column, <code>false</code> if not or if no treeLayer reference is * set to this {@link GroupByComparator} */ protected boolean isTreeColumn(int columnIndex) { if (this.treeLayer != null) { int columnPosition = this.treeLayer.getColumnPositionByIndex(columnIndex); ILayerCell cell = this.treeLayer.getCellByPosition(columnPosition, 0); if (cell != null) { return cell.getConfigLabels().hasLabel(TreeLayer.TREE_COLUMN_CELL); } } // there is no layer set, so we can not determine which column is the // tree column and therefore no column is treated that way return false; } /** * * @param columnIndex * The column index of the column for which the * {@link Comparator} is requested. * @return The {@link Comparator} that should be used to compare the values * for elements in the given column. Returns the * {@link DefaultComparator} in case there is no {@link ISortModel} * configured for this {@link GroupByComparator} or no * {@link Comparator} found for the given column. */ @SuppressWarnings("rawtypes") protected Comparator getComparator(int columnIndex) { Comparator result = null; if (this.sortModel != null) { result = this.sortModel.getColumnComparator(columnIndex); } if (result == null) { result = DefaultComparator.getInstance(); } return result; } @Override public ISortModel getSortModel() { return this.sortModel; } @Override public void setSortModel(ISortModel sortModel) { this.sortModel = sortModel; } @Override public void setTreeLayer(IUniqueIndexLayer treeLayer) { this.treeLayer = treeLayer; } @Override public void setDataLayer(GroupByDataLayer<T> dataLayer) { this.dataLayer = dataLayer; } @Override public void clearCache() { this.summaryColumnCache.clear(); this.groupByObjectComparatorCache.clear(); } /** * * @param groupBy * The {@link GroupByObject} for which the cache information is * requested. Needed to retrieve the information if it is not yet * cached. * @param columnIndex * The column for which the cache information is requested. * @return <code>true</code> if the given column is a summary column, * <code>false</code> if not */ protected Boolean isSummaryColumn(GroupByObject groupBy, int columnIndex) { if (!this.summaryColumnCache.containsKey(columnIndex)) { // build cache information // calling the get method will retrieve and cache the necessary // information getSummaryValueFromCache(groupBy, columnIndex); } return this.summaryColumnCache.get(columnIndex); } /** * Returns the cached summary value for the given {@link GroupByObject} and * column index. If there is no cache information yet, it will be build. * * @param groupBy * The {@link GroupByObject} for which the cache information is * requested. * @param columnIndex * The column for which the cache information is requested. * @return The summary value for the given {@link GroupByObject} and column * index or <code>null</code> in case there is no * {@link GroupByDataLayer} configured in for this * {@link GroupByComparator}. */ protected Object getSummaryValueFromCache(GroupByObject groupBy, int columnIndex) { Object columnCache = null; // it is only possible to retrieve the summary values if the // GroupByDataLayer is set and therefore we only have a cache if it is // set if (this.dataLayer != null) { // check if a cache object is already there GroupByObjectValueCache cache = this.groupByObjectComparatorCache.get(groupBy); if (cache == null) { cache = new GroupByObjectValueCache(); this.groupByObjectComparatorCache.put(groupBy, cache); } // check if the cache object already contains information about the // column columnCache = cache.valueCache.get(columnIndex); if (columnCache == null) { // This is the performance bottleneck that is the reason for the // caching! int rowIndex = this.dataLayer.getTreeList().indexOf(groupBy); if (rowIndex >= 0) { LabelStack labelStack = this.dataLayer.getConfigLabelsByPosition(columnIndex, rowIndex); IGroupBySummaryProvider<T> provider = this.dataLayer.getGroupBySummaryProvider(labelStack); boolean isSummaryColumn = provider != null; this.summaryColumnCache.put(columnIndex, isSummaryColumn); if (isSummaryColumn) { /* * Special Case: If a summary column is grouped, the * summary value is the same as the GroupByObject value. * In that case the roundtrip using the summary provider * is not necessary and we should improve the * performance if the sorting should be applied for that * column. */ // the descriptor map needs to be ordered // (LinkedHashMap) and we need to find the last one in // the order to check if a summary column was used for // grouping Entry<Integer, Object> last = null; for (Entry<Integer, Object> entry : groupBy.getDescriptor().entrySet()) { last = entry; } if (last != null && last.getKey() == columnIndex) { columnCache = groupBy.getValue(); } else { columnCache = this.dataLayer.getDataValueByPosition(columnIndex, rowIndex, labelStack, false); } cache.valueCache.put(columnIndex, columnCache); } } } } else { this.summaryColumnCache.put(columnIndex, false); } return columnCache; } /** * Cache object that holds the the summary values for all summary columns of * a {@link GroupByObject}. */ class GroupByObjectValueCache { Map<Integer, Object> valueCache = new HashMap<Integer, Object>(); } }