Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #121 - [Excel-Filter] Improve performance for huge collections #122

Merged
merged 1 commit into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -93,23 +92,20 @@
*/
public class ComboBoxFilterRowHeaderCompositeIntegrationTest {

private static ArrayList<ExtendedPersonWithAddress> values = new ArrayList<>();
private ArrayList<ExtendedPersonWithAddress> values = new ArrayList<>();

private BodyLayerStack<ExtendedPersonWithAddress> bodyLayer;
private ComboBoxFilterRowHeaderComposite<ExtendedPersonWithAddress> filterRowHeaderLayer;
private NatTableFixture natTable;

private GlazedListsSortModel<ExtendedPersonWithAddress> 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();
Expand Down Expand Up @@ -144,7 +140,7 @@ public void setup() {
// know the ConfigRegistry
this.bodyLayer =
new BodyLayerStack<>(
values,
this.values,
columnPropertyAccessor,
configRegistry);

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
Expand All @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -162,6 +170,9 @@ public void applyFilter(Map<Integer, Object> filterIndexToObjectMap) {

for (Entry<Integer, Object> 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(
Expand All @@ -179,56 +190,71 @@ public void applyFilter(Map<Integer, Object> filterIndexToObjectMap) {
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<>();
EventList<MatcherEditor<T>> 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<String> filterValues = (Set<String>) ((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<ParseResult> parseResults = FilterRowUtils.parse(filterText, textDelimiter, textMatchingMode);

EventList<MatcherEditor<T>> stringMatcherEditors = new BasicEventList<>();
EventList<MatcherEditor<T>> 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<MatcherEditor<T>> allMatcherEditors = new BasicEventList<>();
allMatcherEditors.addAll(stringMatcherEditors);
allMatcherEditors.addAll(thresholdMatcherEditors);
EventList<MatcherEditor<T>> allMatcherEditors = new BasicEventList<>();
allMatcherEditors.addAll(stringMatcherEditors);
allMatcherEditors.addAll(thresholdMatcherEditors);

String[] separator = FilterRowUtils.getSeparatorCharacters(textDelimiter);
String[] separator = FilterRowUtils.getSeparatorCharacters(textDelimiter);

if (!allMatcherEditors.isEmpty()) {
CompositeMatcherEditor<T> 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<T> 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);
}
}

Expand Down Expand Up @@ -422,6 +448,25 @@ protected TextFilterator<T> 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<T> getSetMatcherEditor(Integer columnIndex, Set<String> filterValues, IDisplayConverter converter) {
return new ColumnSetMatcherEditor(columnIndex, filterValues, converter);
}

/**
*
* @param textMatchingMode
Expand Down Expand Up @@ -535,6 +580,8 @@ protected boolean matcherEditorEqual(final MatcherEditor<T> 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);
}
}

Expand Down Expand Up @@ -621,4 +668,86 @@ private DefaultGlazedListsFilterStrategy<T> getOuterType() {
}
}

/**
* Adoption of the GlazedLists SetMatcherEditor.
* <p>
* 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<T> {
private final Integer columnIndex;
private final Set<String> filterValues;
private final IDisplayConverter converter;

public ColumnSetMatcherEditor(Integer columnIndex, Set<String> filterValues, IDisplayConverter converter) {
this.columnIndex = columnIndex;
this.filterValues = filterValues;
this.converter = converter;

if (this.filterValues.isEmpty()) {
this.fireMatchNone();
} else {
this.fireChanged(new SetMatcher<T, String>(
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<E, O> implements Matcher<E> {

private final Set<O> matchSet;
private final Function<E, O> fn;

private SetMatcher(final Set<O> matchSet, final Function<E, O> fn) {
this.matchSet = new HashSet<O>(matchSet);
this.fn = fn;
}

@Override
public boolean matches(final E input) {
boolean result = this.matchSet.contains(this.fn.evaluate(input));
return result;
}
}

private DefaultGlazedListsFilterStrategy<T> getOuterType() {
return DefaultGlazedListsFilterStrategy.this;
}
}
}