/*******************************************************************************
* Copyright (c) 2012, 2013 Original authors 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:
* Original authors and others - initial API and implementation
******************************************************************************/
package org.eclipse.nebula.widgets.nattable.filterrow;
import static org.eclipse.nebula.widgets.nattable.util.ObjectUtils.isEmpty;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.nebula.widgets.nattable.config.CellConfigAttributes;
import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
import org.eclipse.nebula.widgets.nattable.data.IDataProvider;
import org.eclipse.nebula.widgets.nattable.data.convert.IDisplayConverter;
import org.eclipse.nebula.widgets.nattable.filterrow.event.FilterAppliedEvent;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.persistence.IPersistable;
import org.eclipse.nebula.widgets.nattable.style.DisplayMode;
import org.eclipse.nebula.widgets.nattable.util.ObjectUtils;
import org.eclipse.nebula.widgets.nattable.util.PersistenceUtils;
/**
* Data provider for the filter row
* <ul>
* <li>Stores filter strings</li>
* <li>Applies them to the ca.odell.glazedlists.matchers.MatcherEditor on the
* ca.odell.glazedlists.FilterList</li>
* </ul>
*/
public class FilterRowDataProvider<T> implements IDataProvider, IPersistable {
private static final Log log = LogFactory
.getLog(FilterRowDataProvider.class);
/**
* Replacement for the pipe character | that is used for persistence. If
* regular expressions are used for filtering, the pipe character can be
* used in the regular expression to specify alternations. As the
* persistence mechanism in NatTable uses the pipe character for separation
* of values, the persistence breaks for such cases. By replacing the pipe
* in the regular expression with some silly uncommon value specified here,
* we ensure to be able to also persist pipes in the regular expressions,
* aswell as being backwards compatible with already saved filter row
* states.
*/
public static final String PIPE_REPLACEMENT = "°~°"; //$NON-NLS-1$
/**
* The prefix String that will be used to mark that the following filter
* value in the persisted state is a collection.
*/
public static final String FILTER_COLLECTION_PREFIX = "°coll("; //$NON-NLS-1$
/**
* The {@link IFilterStrategy} to which the set filter value should be
* applied.
*/
private final IFilterStrategy<T> filterStrategy;
/**
* The column header layer where this {@link IDataProvider} is used for
* filtering. Needed for retrieval of column indexes and firing according
* filter events.
*/
private final ILayer columnHeaderLayer;
/**
* The {@link IDataProvider} of the column header. This is necessary to
* retrieve the real column count of the column header and not a transformed
* one. (e.g. hiding a column would change the column count in the column
* header but not in the column header {@link IDataProvider}).
*/
private final IDataProvider columnHeaderDataProvider;
/**
* The {@link IConfigRegistry} needed to retrieve the
* {@link IDisplayConverter} for converting the values on state save/load
* operations.
*/
private final IConfigRegistry configRegistry;
/**
* Contains the filter objects mapped to the column index. Basically the
* data storage for the set filters in the filter row so they are visible to
* the user who entered them.
*/
private Map<Integer, Object> filterIndexToObjectMap = new HashMap<Integer, Object>();
/**
*
* @param filterStrategy
* The {@link IFilterStrategy} to which the set filter value
* should be applied.
* @param columnHeaderLayer
* The column header layer where this {@link IDataProvider} is
* used for filtering needed for retrieval of column indexes and
* firing according filter events..
* @param columnHeaderDataProvider
* The {@link IDataProvider} of the column header needed to
* retrieve the real column count of the column header and not a
* transformed one.
* @param configRegistry
* The {@link IConfigRegistry} needed to retrieve the
* {@link IDisplayConverter} for converting the values on state
* save/load operations.
*/
public FilterRowDataProvider(IFilterStrategy<T> filterStrategy,
ILayer columnHeaderLayer, IDataProvider columnHeaderDataProvider,
IConfigRegistry configRegistry) {
this.filterStrategy = filterStrategy;
this.columnHeaderLayer = columnHeaderLayer;
this.columnHeaderDataProvider = columnHeaderDataProvider;
this.configRegistry = configRegistry;
}
/**
* Returns the map that contains the filter objects mapped to the column
* index. It is the data storage for the inserted filters into the filter
* row by the user.
* <p>
* Note: Usually it is not intended to modify this Map directly. You should
* rather call <code>setDataValue(int, int, Object)</code> or
* <code>clearAllFilters()</code> to modify this Map to ensure consistency
* in other framework code. It is made visible because there might be code
* that needs to modify the Map without index transformations or firing
* events.
*
* @return Map that contains the filter objects mapped to the column index.
*/
public Map<Integer, Object> getFilterIndexToObjectMap() {
return this.filterIndexToObjectMap;
}
/**
* Set the map that contains the filter objects mapped to the column index
* to be the data storage for the inserted filters into the filter row by
* the user.
* <p>
* Note: Usually it is not intended to set this Map from the outside as it
* is created in the constructor. But there might be use cases where you
* e.g. need to connect filter rows to each other. In this case it might be
* useful to override the local Map with the one form another
* FilterRowDataProvider. This is not a typical use case, therefore you
* should use this method carefully!
*
* @param filterIndexToObjectMap
* Map that contains the filter objects mapped to the column
* index.
*/
public void setFilterIndexToObjectMap(
Map<Integer, Object> filterIndexToObjectMap) {
this.filterIndexToObjectMap = filterIndexToObjectMap;
}
@Override
public int getColumnCount() {
return this.columnHeaderDataProvider.getColumnCount();
}
@Override
public Object getDataValue(int columnIndex, int rowIndex) {
return this.filterIndexToObjectMap.get(columnIndex);
}
@Override
public int getRowCount() {
return 1;
}
@Override
public void setDataValue(int columnIndex, int rowIndex, Object newValue) {
if (ObjectUtils.isNotNull(newValue)) {
this.filterIndexToObjectMap.put(columnIndex, newValue);
} else {
this.filterIndexToObjectMap.remove(columnIndex);
}
this.filterStrategy.applyFilter(this.filterIndexToObjectMap);
this.columnHeaderLayer.fireLayerEvent(new FilterAppliedEvent(
this.columnHeaderLayer));
}
// Load/save state
@Override
public void saveState(String prefix, Properties properties) {
Map<Integer, String> filterTextByIndex = new HashMap<Integer, String>();
for (Integer columnIndex : this.filterIndexToObjectMap.keySet()) {
final IDisplayConverter converter = this.configRegistry
.getConfigAttribute(CellConfigAttributes.DISPLAY_CONVERTER,
DisplayMode.NORMAL,
FilterRowDataLayer.FILTER_ROW_COLUMN_LABEL_PREFIX
+ columnIndex);
String filterText = getFilterStringRepresentation(
this.filterIndexToObjectMap.get(columnIndex), converter);
filterText = filterText.replace("|", PIPE_REPLACEMENT); //$NON-NLS-1$
filterTextByIndex.put(columnIndex, filterText);
}
String string = PersistenceUtils.mapAsString(filterTextByIndex);
if (!isEmpty(string)) {
properties.put(prefix
+ FilterRowDataLayer.PERSISTENCE_KEY_FILTER_ROW_TOKENS,
string);
}
}
@Override
public void loadState(String prefix, Properties properties) {
this.filterIndexToObjectMap.clear();
try {
Object property = properties.get(prefix
+ FilterRowDataLayer.PERSISTENCE_KEY_FILTER_ROW_TOKENS);
Map<Integer, String> filterTextByIndex = PersistenceUtils
.parseString(property);
for (Integer columnIndex : filterTextByIndex.keySet()) {
final IDisplayConverter converter = this.configRegistry
.getConfigAttribute(
CellConfigAttributes.DISPLAY_CONVERTER,
DisplayMode.NORMAL,
FilterRowDataLayer.FILTER_ROW_COLUMN_LABEL_PREFIX
+ columnIndex);
String filterText = filterTextByIndex.get(columnIndex);
filterText = filterText.replace(PIPE_REPLACEMENT, "|"); //$NON-NLS-1$
this.filterIndexToObjectMap.put(columnIndex,
getFilterFromString(filterText, converter));
}
} catch (Exception e) {
log.error("Error while restoring filter row text!", e); //$NON-NLS-1$
}
this.filterStrategy.applyFilter(this.filterIndexToObjectMap);
}
/**
* This method is used to support saving of a filter collection, e.g. in the
* context of the Excel like filter row. In such cases the filter value is
* not a simple String but a Collection of filter values that need to be
* converted to a String representation. As the state persistence is
* encapsulated to be handled here, we need to take care of such states here
* also.
*
* @param filterValue
* The filter value object that is used for filtering.
* @param converter
* The converter that is used to convert the filter value, which
* is necessary to support filtering of custom types.
* @return The String representation of the filter value.
*/
private String getFilterStringRepresentation(Object filterValue,
IDisplayConverter converter) {
// in case the filter value is a collection of values, we need to create
// a special
// string representation
if (filterValue instanceof Collection) {
String collectionSpec = FILTER_COLLECTION_PREFIX
+ filterValue.getClass().getName() + ")"; //$NON-NLS-1$
StringBuilder builder = new StringBuilder(collectionSpec);
builder.append("["); //$NON-NLS-1$
Collection<?> filterCollection = (Collection<?>) filterValue;
for (Iterator<?> iterator = filterCollection.iterator(); iterator
.hasNext();) {
Object filterObject = iterator.next();
builder.append(converter.canonicalToDisplayValue(filterObject));
if (iterator.hasNext())
builder.append(IPersistable.VALUE_SEPARATOR);
}
builder.append("]"); //$NON-NLS-1$
return builder.toString();
}
return (String) converter.canonicalToDisplayValue(filterValue);
}
/**
* This method is used to support loading of a filter collection, e.g. in
* the context of the Excel like filter row. In such cases the saved filter
* value is not a simple String but represents a Collection of filter values
* that need to be converted to the corresponding values. As the state
* persistence is encapsulated to be handled here, we need to take care of
* such states here also.
*
* @param filterText
* The String representation of the applied saved filter.
* @param converter
* The converter that is used to convert the filter value, which
* is necessary to support filtering of custom types.
* @return The filter value that will be used to apply a filter to the
* IFilterStrategy
*
* @throws InstantiationException
* @throws IllegalAccessException
* @throws ClassNotFoundException
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
private Object getFilterFromString(String filterText,
IDisplayConverter converter) throws InstantiationException,
IllegalAccessException, ClassNotFoundException {
if (filterText.startsWith(FILTER_COLLECTION_PREFIX)) {
// the filter text represents a collection
int indexEndCollSpec = filterText.indexOf(")"); //$NON-NLS-1$
String collectionSpec = filterText.substring(
filterText.indexOf("(") + 1, indexEndCollSpec); //$NON-NLS-1$
Collection filterCollection = (Collection) Class.forName(
collectionSpec).newInstance();
// also get rid of the collection marks
filterText = filterText.substring(indexEndCollSpec + 2,
filterText.length() - 1);
String[] filterSplit = filterText
.split(IPersistable.VALUE_SEPARATOR);
for (String filterString : filterSplit) {
filterCollection.add(converter
.displayToCanonicalValue(filterString));
}
return filterCollection;
}
return converter.displayToCanonicalValue(filterText);
}
/**
* Clear all filters that are currently applied.
*/
public void clearAllFilters() {
this.filterIndexToObjectMap.clear();
this.filterStrategy.applyFilter(this.filterIndexToObjectMap);
}
}