/*******************************************************************************
* Copyright (c) 2013, 2016 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 454505
*******************************************************************************/
package org.eclipse.nebula.widgets.nattable.filterrow.combobox;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.nebula.widgets.nattable.data.IColumnAccessor;
import org.eclipse.nebula.widgets.nattable.edit.editor.IComboBoxDataProvider;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.layer.ILayerListener;
import org.eclipse.nebula.widgets.nattable.layer.event.CellVisualChangeEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.IStructuralChangeEvent;
/**
* IComboBoxDataProvider that provides items for a combobox in the filter row.
* These items are calculated dynamically based on the content contained in the
* column it is connected to.
* <p>
* On creating this IComboBoxDataProvider, the possible values for all columns
* will be calculated taking the whole data provided by the body IDataProvider
* into account. Therefore you shouldn't use this one if you show huge datasets
* at once.
* <p>
* As the values are cached in here, this IComboBoxDataProvider registers itself
* as ILayerListener to the body DataLayer. If values are updated or rows get
* added/deleted, it will update the cache accordingly.
*
* @param <T>
* The type of the objects shown within the NatTable. Needed to
* access the data columnwise.
*/
public class FilterRowComboBoxDataProvider<T> implements IComboBoxDataProvider, ILayerListener {
/**
* The base collection used to collect the unique values from. This need to
* be a collection that is not filtered, otherwise after modifications the
* content of the filter row combo boxes will only contain the current
* visible (not filtered) elements.
*/
private Collection<T> baseCollection;
/**
* The IColumnAccessor to be able to read the values out of the base
* collection objects.
*/
private IColumnAccessor<T> columnAccessor;
/**
* The local cache for the values to show in the filter row combobox. This
* is needed because otherwise the calculation of the necessary values would
* happen everytime the combobox is opened and if a filter is applied using
* GlazedLists for example, the combobox would only contain the value which
* is currently used for filtering.
*/
private final Map<Integer, List<?>> valueCache = new HashMap<Integer, List<?>>();
/**
* List of listeners that get informed if the value cache gets updated.
*/
private List<IFilterRowComboUpdateListener> cacheUpdateListener = new ArrayList<IFilterRowComboUpdateListener>();
/**
* Flag to indicate whether the combo box content should be loaded lazily.
*
* @since 1.4
*/
protected final boolean lazyLoading;
/**
* Flag for enabling/disabling caching of filter combo box values.
*
* @since 1.4
*/
protected boolean cachingEnabled = true;
/**
* @param bodyLayer
* A layer in the body region. Usually the DataLayer or a layer
* that is responsible for list event handling. Needed to
* register ourself as listener for data changes.
* @param baseCollection
* The base collection used to collect the unique values from.
* This need to be a collection that is not filtered, otherwise
* after modifications the content of the filter row combo boxes
* will only contain the current visible (not filtered) elements.
* @param columnAccessor
* The IColumnAccessor to be able to read the values out of the
* base collection objects.
*/
public FilterRowComboBoxDataProvider(
ILayer bodyLayer, Collection<T> baseCollection, IColumnAccessor<T> columnAccessor) {
this(bodyLayer, baseCollection, columnAccessor, true);
}
/**
* @param bodyLayer
* A layer in the body region. Usually the DataLayer or a layer
* that is responsible for list event handling. Needed to
* register ourself as listener for data changes.
* @param baseCollection
* The base collection used to collect the unique values from.
* This need to be a collection that is not filtered, otherwise
* after modifications the content of the filter row combo boxes
* will only contain the current visible (not filtered) elements.
* @param columnAccessor
* The IColumnAccessor to be able to read the values out of the
* base collection objects.
* @param lazy
* <code>true</code> to configure this
* {@link FilterRowComboBoxDataProvider} should load the combobox
* values lazily, <code>false</code> to pre-build the value
* cache.
* @since 1.4
*/
public FilterRowComboBoxDataProvider(
ILayer bodyLayer,
Collection<T> baseCollection,
IColumnAccessor<T> columnAccessor,
boolean lazy) {
this.baseCollection = baseCollection;
this.columnAccessor = columnAccessor;
this.lazyLoading = lazy;
if (!this.lazyLoading) {
// build the cache
buildValueCache();
}
bodyLayer.addLayerListener(this);
}
@Override
public List<?> getValues(int columnIndex, int rowIndex) {
if (this.cachingEnabled) {
List<?> result = this.valueCache.get(columnIndex);
if (result == null) {
result = collectValues(columnIndex);
this.valueCache.put(columnIndex, result);
fireCacheUpdateEvent(buildUpdateEvent(columnIndex, null, result));
}
return result;
} else {
return collectValues(columnIndex);
}
}
/**
* Builds the local value cache for all columns.
*/
protected void buildValueCache() {
for (int i = 0; i < this.columnAccessor.getColumnCount(); i++) {
this.valueCache.put(i, collectValues(i));
}
}
/**
* This method returns the column indexes of the columns for which values
* was cached. Usually it will return all column indexes that are available
* in the table.
*
* @return The column indexes of the columns for which values was cached.
*/
public Collection<Integer> getCachedColumnIndexes() {
return this.valueCache.keySet();
}
/**
* Iterates over all rows of the local body IDataProvider and collects the
* unique values for the given column index.
*
* @param columnIndex
* The column index for which the values should be collected
* @return List of all unique values that are contained in the body
* IDataProvider for the given column.
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
protected List<?> collectValues(int columnIndex) {
Set uniqueValues = new HashSet();
boolean nullFound = false;
for (T rowObject : this.baseCollection) {
Object dataValue = this.columnAccessor.getDataValue(rowObject, columnIndex);
if (dataValue != null) {
uniqueValues.add(dataValue);
} else {
nullFound = true;
}
}
List result = new ArrayList(uniqueValues);
if (!result.isEmpty() && result.get(0) instanceof Comparable) {
Collections.sort(result);
}
if (nullFound) {
result.add(0, null);
}
return result;
}
@Override
public void handleLayerEvent(ILayerEvent event) {
// we only need to perform event handling if caching is enabled
if (this.cachingEnabled) {
if (event instanceof CellVisualChangeEvent) {
// usually this is fired for data updates
// so we need to update the value cache for the updated column
int column = ((CellVisualChangeEvent) event).getColumnPosition();
List<?> cacheBefore = this.valueCache.get(column);
this.valueCache.put(column, collectValues(column));
// get the diff and fire the event
fireCacheUpdateEvent(buildUpdateEvent(column, cacheBefore, this.valueCache.get(column)));
} else if (event instanceof IStructuralChangeEvent
&& ((IStructuralChangeEvent) event).isVerticalStructureChanged()) {
// a new row was added or a row was deleted
// remember the cache before updating
Map<Integer, List<?>> cacheBefore = new HashMap<Integer, List<?>>(this.valueCache);
// perform a refresh of the whole cache
this.valueCache.clear();
if (!this.lazyLoading) {
buildValueCache();
}
// fire events for every column
for (Map.Entry<Integer, List<?>> entry : cacheBefore.entrySet()) {
fireCacheUpdateEvent(buildUpdateEvent(entry.getKey(), entry.getValue(), this.valueCache.get(entry.getKey())));
}
}
}
}
/**
* Creates a FilterRowComboUpdateEvent for the given column index.
* Calculates the diffs of the value cache for that column based on the
* given lists.
*
* @param columnIndex
* The column index for which the value cache was updated.
* @param cacheBefore
* The value cache for the column before the change. Needed to
* determine which values where removed by the update.
* @param cacheAfter
* The value cache for the column after the change. Needed to
* determine which values where added by the update.
* @return Event to tell about value cache updates for the given column or
* <code>null</code> if nothing has changed.
*/
protected FilterRowComboUpdateEvent buildUpdateEvent(int columnIndex, List<?> cacheBefore, List<?> cacheAfter) {
Set<Object> addedValues = new HashSet<Object>();
Set<Object> removedValues = new HashSet<Object>();
// find the added values
if (cacheAfter != null && cacheBefore != null) {
for (Object after : cacheAfter) {
if (!cacheBefore.contains(after)) {
addedValues.add(after);
}
}
// find the removed values
for (Object before : cacheBefore) {
if (!cacheAfter.contains(before)) {
removedValues.add(before);
}
}
} else if ((cacheBefore == null || cacheBefore.isEmpty()) && cacheAfter != null) {
addedValues.addAll(cacheAfter);
} else if (cacheBefore != null && (cacheAfter == null || cacheAfter.isEmpty())) {
removedValues.addAll(cacheBefore);
}
// only create a new update event if there has something changed
if (!addedValues.isEmpty() || !removedValues.isEmpty()) {
return new FilterRowComboUpdateEvent(columnIndex, addedValues, removedValues);
}
// nothing has changed so nothing to update
return null;
}
/**
* Fire the given event to all registered listeners.
*
* @param event
* The event to handle.
*/
protected void fireCacheUpdateEvent(FilterRowComboUpdateEvent event) {
if (event != null) {
for (IFilterRowComboUpdateListener listener : this.cacheUpdateListener) {
listener.handleEvent(event);
}
}
}
/**
* Adds the given listener to the list of listeners for value cache updates.
*
* @param listener
* The listener to add.
*/
public void addCacheUpdateListener(IFilterRowComboUpdateListener listener) {
this.cacheUpdateListener.add(listener);
}
/**
* Removes the given listener from the list of listeners for value cache
* updates.
*
* @param listener
* The listener to remove.
*/
public void removeCacheUdpateListener(IFilterRowComboUpdateListener listener) {
this.cacheUpdateListener.remove(listener);
}
/**
* @return The local cache for the values to show in the filter row
* combobox. This is needed because otherwise the calculation of the
* necessary values would happen everytime the combobox is opened
* and if a filter is applied using GlazedLists for example, the
* combobox would only contain the value which is currently used for
* filtering.
*/
protected Map<Integer, List<?>> getValueCache() {
return this.valueCache;
}
/**
*
* @return <code>true</code> if caching of filterrow combobox values is
* enabled, <code>false</code> if the combobox values should be
* calculated on request.
* @since 1.4
*/
public boolean isCachingEnabled() {
return this.cachingEnabled;
}
/**
* Enable/disable the caching of filterrow combobox values. By default the
* caching is enabled.
* <p>
* You should disable caching if the base collection that is used to
* determine the filterrow combobox values changes its contents dynamically,
* e.g. if the base collection is a GlazedLists FilterList that returns only
* the current non-filtered items.
* </p>
*
* @param cachingEnabled
* <code>true</code> to enable caching of filter row combobox
* values, <code>false</code> if the combobox values should be
* calculated on request.
* @since 1.4
*/
public void setCachingEnabled(boolean cachingEnabled) {
this.cachingEnabled = cachingEnabled;
}
/**
* Cleanup acquired resources.
*
* @since 1.5
*/
public void dispose() {
// nothing to do here
}
}