From d70fb8f81438e2e456f538feabe2c366499a975e Mon Sep 17 00:00:00 2001 From: Dirk Fauth Date: Tue, 8 Oct 2024 16:21:43 +0200 Subject: [PATCH] Fixes #121 - [Excel-Filter] Improve performance for huge collections Signed-off-by: Dirk Fauth --- ...lterRowHeaderCompositeIntegrationTest.java | 16 +- .../DefaultGlazedListsFilterStrategy.java | 215 ++++++++++++++---- 2 files changed, 178 insertions(+), 53 deletions(-) diff --git a/org.eclipse.nebula.widgets.nattable.extension.glazedlists.test/src/org/eclipse/nebula/widgets/nattable/extension/glazedlists/filterrow/ComboBoxFilterRowHeaderCompositeIntegrationTest.java b/org.eclipse.nebula.widgets.nattable.extension.glazedlists.test/src/org/eclipse/nebula/widgets/nattable/extension/glazedlists/filterrow/ComboBoxFilterRowHeaderCompositeIntegrationTest.java index 8118c9aa6..09f9feab4 100644 --- a/org.eclipse.nebula.widgets.nattable.extension.glazedlists.test/src/org/eclipse/nebula/widgets/nattable/extension/glazedlists/filterrow/ComboBoxFilterRowHeaderCompositeIntegrationTest.java +++ b/org.eclipse.nebula.widgets.nattable.extension.glazedlists.test/src/org/eclipse/nebula/widgets/nattable/extension/glazedlists/filterrow/ComboBoxFilterRowHeaderCompositeIntegrationTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019, 2022 Dirk Fauth. + * Copyright (c) 2019, 2024 Dirk Fauth. * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -72,7 +72,6 @@ import org.eclipse.nebula.widgets.nattable.tree.TreeLayer; import org.eclipse.nebula.widgets.nattable.viewport.ViewportLayer; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -93,7 +92,7 @@ */ public class ComboBoxFilterRowHeaderCompositeIntegrationTest { - private static ArrayList values = new ArrayList<>(); + private ArrayList values = new ArrayList<>(); private BodyLayerStack bodyLayer; private ComboBoxFilterRowHeaderComposite filterRowHeaderLayer; @@ -101,15 +100,12 @@ public class ComboBoxFilterRowHeaderCompositeIntegrationTest { private GlazedListsSortModel sortModel; - @BeforeAll - public static void setupClass() { + @BeforeEach + public void setup() { for (int i = 0; i < 300; i++) { - values.addAll(createValues(i * 30)); + this.values.addAll(createValues(i * 30)); } - } - @BeforeEach - public void setup() { // create a new ConfigRegistry which will be needed for GlazedLists // handling ConfigRegistry configRegistry = new ConfigRegistry(); @@ -144,7 +140,7 @@ public void setup() { // know the ConfigRegistry this.bodyLayer = new BodyLayerStack<>( - values, + this.values, columnPropertyAccessor, configRegistry); diff --git a/org.eclipse.nebula.widgets.nattable.extension.glazedlists/src/org/eclipse/nebula/widgets/nattable/extension/glazedlists/filterrow/DefaultGlazedListsFilterStrategy.java b/org.eclipse.nebula.widgets.nattable.extension.glazedlists/src/org/eclipse/nebula/widgets/nattable/extension/glazedlists/filterrow/DefaultGlazedListsFilterStrategy.java index c3465f87e..09cfaa58c 100644 --- a/org.eclipse.nebula.widgets.nattable.extension.glazedlists/src/org/eclipse/nebula/widgets/nattable/extension/glazedlists/filterrow/DefaultGlazedListsFilterStrategy.java +++ b/org.eclipse.nebula.widgets.nattable.extension.glazedlists/src/org/eclipse/nebula/widgets/nattable/extension/glazedlists/filterrow/DefaultGlazedListsFilterStrategy.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2012, 2023 Original authors and others. + * Copyright (c) 2012, 2024 Original authors and others. * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -13,12 +13,17 @@ ******************************************************************************/ package org.eclipse.nebula.widgets.nattable.extension.glazedlists.filterrow; +import java.util.Collection; import java.util.Comparator; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; import java.util.regex.PatternSyntaxException; +import java.util.stream.Collectors; import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry; import org.eclipse.nebula.widgets.nattable.data.IColumnAccessor; @@ -31,6 +36,7 @@ import org.eclipse.nebula.widgets.nattable.filterrow.config.FilterRowConfigAttributes; import org.eclipse.nebula.widgets.nattable.layer.cell.LayerCell; import org.eclipse.nebula.widgets.nattable.style.DisplayMode; +import org.eclipse.nebula.widgets.nattable.util.ObjectUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,7 +47,9 @@ import ca.odell.glazedlists.FunctionList.Function; import ca.odell.glazedlists.GlazedLists; import ca.odell.glazedlists.TextFilterator; +import ca.odell.glazedlists.matchers.AbstractMatcherEditor; import ca.odell.glazedlists.matchers.CompositeMatcherEditor; +import ca.odell.glazedlists.matchers.Matcher; import ca.odell.glazedlists.matchers.MatcherEditor; import ca.odell.glazedlists.matchers.Matchers; import ca.odell.glazedlists.matchers.TextMatcherEditor; @@ -162,6 +170,9 @@ public void applyFilter(Map filterIndexToObjectMap) { for (Entry mapEntry : filterIndexToObjectMap.entrySet()) { Integer columnIndex = mapEntry.getKey(); + // we create the filterText before accessing the other + // configuration values, because a converter might change the + // configuration dynamically based on the value String filterText = getStringFromColumnObject(columnIndex, mapEntry.getValue()); String textDelimiter = this.configRegistry.getConfigAttribute( @@ -179,56 +190,71 @@ public void applyFilter(Map filterIndexToObjectMap) { FilterRowDataLayer.FILTER_ROW_COLUMN_LABEL_PREFIX + columnIndex); final Function columnValueProvider = getColumnValueProvider(columnIndex); - List parseResults = FilterRowUtils.parse(filterText, textDelimiter, textMatchingMode); - - EventList> stringMatcherEditors = new BasicEventList<>(); - EventList> thresholdMatcherEditors = new BasicEventList<>(); - 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()); - thresholdMatcherEditors.add(getThresholdMatcherEditor( - columnIndex, - threshold, - comparator, - columnValueProvider, - matchOperation)); + if (mapEntry.getValue() instanceof Collection && textMatchingMode.equals(TextMatchingMode.EXACT)) { + // if the filter value is a collection and the + // TextMatchingMode is EXACT the most efficient way is using + // a SetMatcherEditor + Set filterValues = (Set) ((Collection) mapEntry.getValue()) + .stream() + .map(v -> getStringFromColumnObject(columnIndex, v)) + .collect(Collectors.toSet()); + matcherEditors.add(getSetMatcherEditor(columnIndex, filterValues, displayConverter)); + + } else { + // if the filter value is not a collection, or it is a + // collection but the TextMatchingMode is REGULAR_EXPRESSION + // process the filter value as string + List parseResults = FilterRowUtils.parse(filterText, textDelimiter, textMatchingMode); + + EventList> stringMatcherEditors = new BasicEventList<>(); + EventList> thresholdMatcherEditors = new BasicEventList<>(); + 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()); + thresholdMatcherEditors.add(getThresholdMatcherEditor( + columnIndex, + threshold, + comparator, + columnValueProvider, + matchOperation)); + } + } catch (PatternSyntaxException e) { + LOG.warn("Error on applying a filter: {}", e.getLocalizedMessage()); //$NON-NLS-1$ } - } catch (PatternSyntaxException e) { - LOG.warn("Error on applying a filter: {}", e.getLocalizedMessage()); //$NON-NLS-1$ } - } - EventList> allMatcherEditors = new BasicEventList<>(); - allMatcherEditors.addAll(stringMatcherEditors); - allMatcherEditors.addAll(thresholdMatcherEditors); + EventList> allMatcherEditors = new BasicEventList<>(); + allMatcherEditors.addAll(stringMatcherEditors); + allMatcherEditors.addAll(thresholdMatcherEditors); - String[] separator = FilterRowUtils.getSeparatorCharacters(textDelimiter); + String[] separator = FilterRowUtils.getSeparatorCharacters(textDelimiter); - if (!allMatcherEditors.isEmpty()) { - CompositeMatcherEditor allCompositeMatcherEditor = new CompositeMatcherEditor<>(allMatcherEditors); - if (!thresholdMatcherEditors.isEmpty()) { - if (separator == null || filterText.contains(separator[0])) { - allCompositeMatcherEditor.setMode(CompositeMatcherEditor.AND); - } else { - allCompositeMatcherEditor.setMode(CompositeMatcherEditor.OR); - } - } else { - if (separator == null || filterText.contains(separator[1])) { - allCompositeMatcherEditor.setMode(CompositeMatcherEditor.OR); + if (!allMatcherEditors.isEmpty()) { + CompositeMatcherEditor allCompositeMatcherEditor = new CompositeMatcherEditor<>(allMatcherEditors); + if (!thresholdMatcherEditors.isEmpty()) { + if (separator == null || filterText.contains(separator[0])) { + allCompositeMatcherEditor.setMode(CompositeMatcherEditor.AND); + } else { + allCompositeMatcherEditor.setMode(CompositeMatcherEditor.OR); + } } else { - allCompositeMatcherEditor.setMode(CompositeMatcherEditor.AND); + if (separator == null || filterText.contains(separator[1])) { + allCompositeMatcherEditor.setMode(CompositeMatcherEditor.OR); + } else { + allCompositeMatcherEditor.setMode(CompositeMatcherEditor.AND); + } } + matcherEditors.add(allCompositeMatcherEditor); } - matcherEditors.add(allCompositeMatcherEditor); } } @@ -422,6 +448,25 @@ protected TextFilterator getTextFilterator(final Integer columnIndex, final I return new ColumnTextFilterator(converter, columnIndex); } + /** + * Sets up a {@link MatcherEditor} for a collection of Strings. + * + * @param columnIndex + * the column index of the column for which the matcher editor is + * being set up + * @param filterValues + * the values entered by the user in the filter row + * @param converter + * The {@link IDisplayConverter} used for converting the cell + * value to a String + * @return A {@link ColumnSetMatcherEditor} based on the given information. + * + * @since 2.5 + */ + protected MatcherEditor getSetMatcherEditor(Integer columnIndex, Set filterValues, IDisplayConverter converter) { + return new ColumnSetMatcherEditor(columnIndex, filterValues, converter); + } + /** * * @param textMatchingMode @@ -535,6 +580,8 @@ protected boolean matcherEditorEqual(final MatcherEditor first, final Matcher // MatchOperation is not visible and must be a // references instance, so the 'equals' is not needed && firstThreshold.getMatchOperation() == secondThreshold.getMatchOperation(); + } else { + result = first.equals(second); } } @@ -621,4 +668,86 @@ private DefaultGlazedListsFilterStrategy getOuterType() { } } + /** + * Adoption of the GlazedLists SetMatcherEditor. + *

+ * It does not support different modes, as our logic for empty collections + * is EMPTY_MATCH_NONE. It additionally provides state informations, which + * allows us to identify an equal MatcherEditor and remove it instead of + * updating an existing one. This is needed because we do not have a single + * instance of the MatcherEditors. We create them on demand for every column + * that has a filter configured. + * + * @since 2.5 + */ + public class ColumnSetMatcherEditor extends AbstractMatcherEditor { + private final Integer columnIndex; + private final Set filterValues; + private final IDisplayConverter converter; + + public ColumnSetMatcherEditor(Integer columnIndex, Set filterValues, IDisplayConverter converter) { + this.columnIndex = columnIndex; + this.filterValues = filterValues; + this.converter = converter; + + if (this.filterValues.isEmpty()) { + this.fireMatchNone(); + } else { + this.fireChanged(new SetMatcher( + filterValues, + t -> { + Object cellData = DefaultGlazedListsFilterStrategy.this.columnAccessor.getDataValue(t, columnIndex); + Object displayValue = this.converter.canonicalToDisplayValue(cellData); + displayValue = (displayValue != null) ? displayValue : ""; //$NON-NLS-1$ + return displayValue.toString(); + })); + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + getOuterType().hashCode(); + result = prime * result + Objects.hash(this.columnIndex, this.filterValues); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + @SuppressWarnings("unchecked") + ColumnSetMatcherEditor other = (ColumnSetMatcherEditor) obj; + if (!getOuterType().equals(other.getOuterType())) + return false; + return Objects.equals(this.columnIndex, other.columnIndex) + && ObjectUtils.collectionsEqual(this.filterValues, other.filterValues); + } + + private class SetMatcher implements Matcher { + + private final Set matchSet; + private final Function fn; + + private SetMatcher(final Set matchSet, final Function fn) { + this.matchSet = new HashSet(matchSet); + this.fn = fn; + } + + @Override + public boolean matches(final E input) { + boolean result = this.matchSet.contains(this.fn.evaluate(input)); + return result; + } + } + + private DefaultGlazedListsFilterStrategy getOuterType() { + return DefaultGlazedListsFilterStrategy.this; + } + } }