| /******************************************************************************* |
| * Copyright (c) 2012, 2020 Original authors and others. |
| * |
| * This program and the accompanying materials are made |
| * available under the terms of the Eclipse Public License 2.0 |
| * which is available at https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Original authors and others - initial API and implementation |
| ******************************************************************************/ |
| package org.eclipse.nebula.widgets.nattable.filterrow; |
| |
| import java.lang.reflect.InvocationTargetException; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.Properties; |
| |
| 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; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * 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 Logger LOG = LoggerFactory.getLogger(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, as |
| * well 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<>(); |
| |
| /** |
| * |
| * @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<>(); |
| 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 (!ObjectUtils.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.columnHeaderLayer.fireLayerEvent(new FilterAppliedEvent(this.columnHeaderLayer)); |
| } |
| |
| /** |
| * 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 |
| * @throws SecurityException |
| * @throws NoSuchMethodException |
| * @throws InvocationTargetException |
| * @throws IllegalArgumentException |
| */ |
| @SuppressWarnings({ "rawtypes", "unchecked" }) |
| private Object getFilterFromString(String filterText, IDisplayConverter converter) |
| throws InstantiationException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException { |
| |
| 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).getDeclaredConstructor().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); |
| |
| this.columnHeaderLayer.fireLayerEvent(new FilterAppliedEvent(this.columnHeaderLayer)); |
| } |
| |
| } |