/******************************************************************************* * Copyright (c) 2012, 2017 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 * Nicolas FAUVERGUE (CEA LIST) nicolas.fauvergue@cea.fr - Bug 508891 ******************************************************************************/ package org.eclipse.nebula.widgets.nattable.extension.glazedlists.filterrow; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.regex.PatternSyntaxException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry; import org.eclipse.nebula.widgets.nattable.data.IColumnAccessor; import org.eclipse.nebula.widgets.nattable.data.convert.IDisplayConverter; import org.eclipse.nebula.widgets.nattable.filterrow.FilterRowDataLayer; import org.eclipse.nebula.widgets.nattable.filterrow.IFilterStrategy; import org.eclipse.nebula.widgets.nattable.filterrow.ParseResult; import org.eclipse.nebula.widgets.nattable.filterrow.ParseResult.MatchType; import org.eclipse.nebula.widgets.nattable.filterrow.TextMatchingMode; import org.eclipse.nebula.widgets.nattable.filterrow.config.FilterRowConfigAttributes; import org.eclipse.nebula.widgets.nattable.style.DisplayMode; import ca.odell.glazedlists.BasicEventList; import ca.odell.glazedlists.EventList; import ca.odell.glazedlists.FilterList; import ca.odell.glazedlists.FunctionList; import ca.odell.glazedlists.FunctionList.Function; import ca.odell.glazedlists.TextFilterator; import ca.odell.glazedlists.matchers.CompositeMatcherEditor; import ca.odell.glazedlists.matchers.MatcherEditor; import ca.odell.glazedlists.matchers.TextMatcherEditor; import ca.odell.glazedlists.matchers.ThresholdMatcherEditor; import ca.odell.glazedlists.util.concurrent.ReadWriteLock; public class DefaultGlazedListsFilterStrategy<T> implements IFilterStrategy<T> { private static final Log LOG = LogFactory.getLog(DefaultGlazedListsFilterStrategy.class); protected final IColumnAccessor<T> columnAccessor; protected final IConfigRegistry configRegistry; private final CompositeMatcherEditor<T> matcherEditor; protected FilterList<T> filterList; protected ReadWriteLock filterLock; /** * Create a new DefaultGlazedListsFilterStrategy on top of the given * FilterList. * <p> * Note: Using this constructor you don't need to create and set the * CompositeMatcherEditor as MatcherEditor on the FilterList yourself! The * necessary steps to get it working is done within this constructor. * </p> * * @param filterList * The FilterList that is used within the GlazedLists based * NatTable for filtering. * @param columnAccessor * The IColumnAccessor necessary to access the column data of the * row objects in the FilterList. * @param configRegistry * The IConfigRegistry necessary to retrieve filter specific * configurations. */ public DefaultGlazedListsFilterStrategy( FilterList<T> filterList, IColumnAccessor<T> columnAccessor, IConfigRegistry configRegistry) { this(filterList, new CompositeMatcherEditor<T>(), columnAccessor, configRegistry); this.matcherEditor.setMode(CompositeMatcherEditor.AND); } /** * Create a new DefaultGlazedListsFilterStrategy on top of the given * FilterList using the given CompositeMatcherEditor. This is necessary to * support connection of multiple filter rows. * <p> * Note: Using this constructor you need to create the * CompositeMatcherEditor yourself. It will be added automatically to the * given FilterList, so you can skip that step. * </p> * * @param filterList * The FilterList that is used within the GlazedLists based * NatTable for filtering. * @param matcherEditor * The CompositeMatcherEditor that should be used by this * DefaultGlazedListsFilterStrategy. * @param columnAccessor * The IColumnAccessor necessary to access the column data of the * row objects in the FilterList. * @param configRegistry * The IConfigRegistry necessary to retrieve filter specific * configurations. */ public DefaultGlazedListsFilterStrategy( FilterList<T> filterList, CompositeMatcherEditor<T> matcherEditor, IColumnAccessor<T> columnAccessor, IConfigRegistry configRegistry) { this.columnAccessor = columnAccessor; this.configRegistry = configRegistry; this.matcherEditor = matcherEditor; this.filterList = filterList; this.filterList.setMatcherEditor(this.matcherEditor); this.filterLock = filterList.getReadWriteLock(); } /** * Create GlazedLists matcher editors and apply them to facilitate * filtering. */ @Override @SuppressWarnings({ "unchecked", "rawtypes" }) public void applyFilter(Map<Integer, Object> filterIndexToObjectMap) { if (filterIndexToObjectMap.isEmpty()) { // wait until all listeners had the chance to handle the clear event try { this.filterLock.writeLock().lock(); this.matcherEditor.getMatcherEditors().clear(); } finally { this.filterLock.writeLock().unlock(); } return; } try { EventList<MatcherEditor<T>> matcherEditors = new BasicEventList<MatcherEditor<T>>(); for (Entry<Integer, Object> mapEntry : filterIndexToObjectMap.entrySet()) { Integer columnIndex = mapEntry.getKey(); String filterText = getStringFromColumnObject(columnIndex, mapEntry.getValue()); String textDelimiter = this.configRegistry.getConfigAttribute( FilterRowConfigAttributes.TEXT_DELIMITER, DisplayMode.NORMAL, FilterRowDataLayer.FILTER_ROW_COLUMN_LABEL_PREFIX + columnIndex); TextMatchingMode textMatchingMode = this.configRegistry.getConfigAttribute( FilterRowConfigAttributes.TEXT_MATCHING_MODE, DisplayMode.NORMAL, FilterRowDataLayer.FILTER_ROW_COLUMN_LABEL_PREFIX + columnIndex); IDisplayConverter displayConverter = this.configRegistry.getConfigAttribute( FilterRowConfigAttributes.FILTER_DISPLAY_CONVERTER, DisplayMode.NORMAL, FilterRowDataLayer.FILTER_ROW_COLUMN_LABEL_PREFIX + columnIndex); Comparator comparator = this.configRegistry.getConfigAttribute( FilterRowConfigAttributes.FILTER_COMPARATOR, DisplayMode.NORMAL, FilterRowDataLayer.FILTER_ROW_COLUMN_LABEL_PREFIX + columnIndex); final Function<T, Object> columnValueProvider = getColumnValueProvider(columnIndex); List<ParseResult> parseResults = FilterRowUtils.parse(filterText, textDelimiter, textMatchingMode); EventList<MatcherEditor<T>> stringMatcherEditors = new BasicEventList<MatcherEditor<T>>(); for (ParseResult parseResult : parseResults) { try { MatchType matchOperation = parseResult.getMatchOperation(); if (matchOperation == MatchType.NONE) { stringMatcherEditors.add(getTextMatcherEditor( columnIndex, textMatchingMode, displayConverter, parseResult.getValueToMatch())); } else { Object threshold = displayConverter.displayToCanonicalValue(parseResult.getValueToMatch()); matcherEditors.add(getThresholdMatcherEditor( columnIndex, threshold, comparator, columnValueProvider, matchOperation)); } } catch (PatternSyntaxException e) { LOG.warn("Error on applying a filter: " + e.getLocalizedMessage()); //$NON-NLS-1$ } } if (stringMatcherEditors.size() > 0) { final CompositeMatcherEditor<T> stringCompositeMatcherEditor = new CompositeMatcherEditor<T>(stringMatcherEditors); stringCompositeMatcherEditor.setMode(CompositeMatcherEditor.OR); matcherEditors.add(stringCompositeMatcherEditor); } } // wait until all listeners had the chance to handle the clear event try { this.filterLock.writeLock().lock(); // Remove the existing matchers that are removed from // 'filterIndexToObjectMap' final Iterator<MatcherEditor<T>> existingMatcherEditors = this.matcherEditor.getMatcherEditors().iterator(); while (existingMatcherEditors.hasNext()) { final MatcherEditor<T> existingMatcherEditor = existingMatcherEditors.next(); if (!containsMatcherEditor(matcherEditors, existingMatcherEditor)) { existingMatcherEditors.remove(); } } // Add the new matchers that are added from // 'filterIndexToObjectMap' for (final MatcherEditor<T> matcherEditor : matcherEditors) { if (!containsMatcherEditor(this.matcherEditor.getMatcherEditors(), matcherEditor)) { this.matcherEditor.getMatcherEditors().add(matcherEditor); } } } finally { this.filterLock.writeLock().unlock(); } } catch (Exception e) { LOG.error("Error on applying a filter", e); //$NON-NLS-1$ } } /** * Converts the object inserted to the filter cell at the given column * position to the corresponding String. * * @param columnIndex * The column index of the filter cell that should be processed. * @param object * The value set to the filter cell that needs to be converted * @return The String value for the given filter value. */ protected String getStringFromColumnObject(final int columnIndex, final Object object) { final IDisplayConverter displayConverter = this.configRegistry.getConfigAttribute( FilterRowConfigAttributes.FILTER_DISPLAY_CONVERTER, DisplayMode.NORMAL, FilterRowDataLayer.FILTER_ROW_COLUMN_LABEL_PREFIX + columnIndex); return displayConverter.canonicalToDisplayValue(object).toString(); } /** * Set up a threshold matcher for tokens like '>20', '<=10' etc. * * @param columnIndex * the column index of the column for which the matcher editor is * being set up * @param threshold * the threshold value used for comparison * @param comparator * {@link Comparator} that is used to determine how objects * compare with the threshold value * @param columnValueProvider * {@link Function} that exposes the content of the given column * index from a row object * @param matchOperation * The NatTable {@link MatchType} used to determine the * GlazedLists ThresholdMatcherEditor#MatchOperation * @return A {@link ThresholdMatcherEditor} that filters elements based on * whether they are greater than or less than a threshold. */ protected ThresholdMatcherEditor<T, Object> getThresholdMatcherEditor( Integer columnIndex, Object threshold, Comparator<Object> comparator, Function<T, Object> columnValueProvider, MatchType matchOperation) { ThresholdMatcherEditor<T, Object> thresholdMatcherEditor = new ThresholdMatcherEditor<T, Object>(threshold, null, comparator, columnValueProvider); FilterRowUtils.setMatchOperation(thresholdMatcherEditor, matchOperation); return thresholdMatcherEditor; } /** * * @param columnIndex * The column index of the column whose contents should be * exposed. * @return {@link Function} which exposes the content of the given column * index from a row object */ protected FunctionList.Function<T, Object> getColumnValueProvider(final int columnIndex) { return new FunctionList.Function<T, Object>() { @Override public Object evaluate(T rowObject) { return DefaultGlazedListsFilterStrategy.this.columnAccessor.getDataValue(rowObject, columnIndex); } }; } /** * Sets up a text matcher editor for String tokens * * @param columnIndex * the column index of the column for which the matcher editor is * being set up * @param textMatchingMode * The NatTable {@link TextMatchingMode} that should be used * @param converter * The {@link IDisplayConverter} used for converting the cell * value to a String * @param filterText * text entered by the user in the filter row * @return A {@link TextMatcherEditor} based on the given information. */ protected TextMatcherEditor<T> getTextMatcherEditor( Integer columnIndex, TextMatchingMode textMatchingMode, IDisplayConverter converter, String filterText) { final TextMatcherEditor<T> textMatcherEditor = new TextMatcherEditor<T>(getTextFilterator(columnIndex, converter)); textMatcherEditor.setFilterText(new String[] { filterText }); textMatcherEditor.setMode(getGlazedListsTextMatcherEditorMode(textMatchingMode)); return textMatcherEditor; } /** * * @param columnIndex * The column index of the column whose contents should be * collected as Strings * @param converter * The {@link IDisplayConverter} used for converting the cell * value to a String * @return {@link TextFilterator} which exposes the contents of the column * as a {@link String} */ protected TextFilterator<T> getTextFilterator(final Integer columnIndex, final IDisplayConverter converter) { return new TextFilterator<T>() { @Override public void getFilterStrings(List<String> objectAsListOfStrings, T rowObject) { Object cellData = DefaultGlazedListsFilterStrategy.this.columnAccessor.getDataValue(rowObject, columnIndex); Object displayValue = converter.canonicalToDisplayValue(cellData); displayValue = (displayValue != null) ? displayValue : ""; //$NON-NLS-1$ objectAsListOfStrings.add(displayValue.toString()); } }; } /** * * @param textMatchingMode * The NatTable TextMatchingMode for which the GlazedLists * {@link TextMatcherEditor} mode is requested * @return The GlazedLists {@link TextMatcherEditor} mode for the given * NatTable {@link TextMatchingMode} */ public int getGlazedListsTextMatcherEditorMode(TextMatchingMode textMatchingMode) { switch (textMatchingMode) { case EXACT: return TextMatcherEditor.EXACT; case STARTS_WITH: return TextMatcherEditor.STARTS_WITH; case REGULAR_EXPRESSION: return TextMatcherEditor.REGULAR_EXPRESSION; default: return TextMatcherEditor.CONTAINS; } } /** * This allows to determinate if the matcher editor in parameter is already * existing in the list of matcher editors as first parameter. This function * takes care of {@link CompositeMatcherEditor}. * * @param existingMatcherEditors * The list of existing matcher editors. * @param matcherEditor * The matcher editor to search. * @return <code>true</code> if the matcher editor is already existing in * the list of matcher editors, <code>false</code> otherwise. * * @since 1.5 */ protected boolean containsMatcherEditor( final List<MatcherEditor<T>> existingMatcherEditors, final MatcherEditor<T> matcherEditor) { boolean result = false; final Iterator<MatcherEditor<T>> existingMatcherEditorsIterator = existingMatcherEditors.iterator(); while (existingMatcherEditorsIterator.hasNext() && !result) { result = matcherEditorEqual(existingMatcherEditorsIterator.next(), matcherEditor); } return result; } /** * This allows to determinate if two matcher editors are equals. * * @param first * The first matcher editor to compare. * @param second * The second matcher editor to compare. * @return <code>true</code> if the matcher editors are equals, * <code>false</code> otherwise. * * @since 1.5 */ protected boolean matcherEditorEqual(final MatcherEditor<T> first, final MatcherEditor<T> second) { boolean result = false; // Compare the matcher classes, and must be equals if (first.getClass().equals(second.getClass())) { if (first instanceof CompositeMatcherEditor) { // Check that the composite matcher editors have the same number // of sub matcher editors CompositeMatcherEditor<T> firstComp = (CompositeMatcherEditor<T>) first; CompositeMatcherEditor<T> secondComp = (CompositeMatcherEditor<T>) second; result = firstComp.getMatcherEditors().size() == secondComp.getMatcherEditors().size(); // Check that all sub matcher editors of first composite matcher // editors are available in the sub matchers of second composite // matcher editor final Iterator<MatcherEditor<T>> matcherEditors = firstComp.getMatcherEditors().iterator(); while (matcherEditors.hasNext() && result) { final Iterator<MatcherEditor<T>> iterator = secondComp.getMatcherEditors().iterator(); boolean found = false; while (iterator.hasNext() && !found) { found = matcherEditorEqual(iterator.next(), matcherEditors.next()); } result = found; } } else if (first instanceof TextMatcherEditor) { TextMatcherEditor<T> firstText = (TextMatcherEditor<T>) first; TextMatcherEditor<T> secondText = (TextMatcherEditor<T>) second; result = first.getMatcher().equals(second.getMatcher()) && firstText.getMode() == secondText.getMode() && firstText.getStrategy().equals(secondText.getStrategy()); } else if (first instanceof ThresholdMatcherEditor) { ThresholdMatcherEditor<?, ?> firstThreshold = (ThresholdMatcherEditor<?, ?>) first; ThresholdMatcherEditor<?, ?> secondThreshold = (ThresholdMatcherEditor<?, ?>) second; result = firstThreshold.getThreshold().equals(secondThreshold.getThreshold()) && firstThreshold.getComparator().equals(secondThreshold.getComparator()) // MatchOperation is not visible and must be a // references instance, so the 'equals' is not needed && firstThreshold.getMatchOperation() == secondThreshold.getMatchOperation(); } } return result; } /** * Returns the {@link CompositeMatcherEditor} that is created and used by * this {@link IFilterStrategy}. In prior versions it was necessary to * create the {@link CompositeMatcherEditor} outside this class and use it * as constructor parameter. We changed this to hide that implementation * from users and to ensure that filter operations and possible listeners * are executed thread safe. Otherwise there might be concurrency issues * while filtering. * <p> * If you want to use additional filtering you should now use this method to * work on the created {@link CompositeMatcherEditor} instead of creating * one outside. For static filtering additional to the filter row you might * want to consider using the * {@link DefaultGlazedListsStaticFilterStrategy}. * </p> * * @return The {@link CompositeMatcherEditor} that is created and used by * this {@link IFilterStrategy}. */ public CompositeMatcherEditor<T> getMatcherEditor() { return this.matcherEditor; } }