EDQS - added SQL like style for filter contains/starts with/ends with

This commit is contained in:
Volodymyr Babak 2025-06-10 19:29:16 +03:00 committed by Andrii Shvaika
parent 2bd7b4d01d
commit dd9d954c2f
5 changed files with 85 additions and 23 deletions

View File

@ -36,7 +36,7 @@ public abstract class AbstractEntityProfileNameQueryProcessor<T extends EntityFi
public AbstractEntityProfileNameQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query, T filter, EntityType entityType) { public AbstractEntityProfileNameQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query, T filter, EntityType entityType) {
super(repo, ctx, query, filter, entityType); super(repo, ctx, query, filter, entityType);
entityProfileNames = new HashSet<>(getProfileNames(this.filter)); entityProfileNames = new HashSet<>(getProfileNames(this.filter));
pattern = RepositoryUtils.toSqlLikePattern(getEntityNameFilter(filter)); pattern = RepositoryUtils.toContainsSqlLikePattern(getEntityNameFilter(filter));
} }
protected abstract String getEntityNameFilter(T filter); protected abstract String getEntityNameFilter(T filter);

View File

@ -43,7 +43,7 @@ public abstract class AbstractEntityProfileQueryProcessor<T extends EntityFilter
entityProfileIds.add(dp.getId()); entityProfileIds.add(dp.getId());
} }
} }
pattern = RepositoryUtils.toSqlLikePattern(getEntityNameFilter(filter)); pattern = RepositoryUtils.toContainsSqlLikePattern(getEntityNameFilter(filter));
} }
protected abstract String getEntityNameFilter(T filter); protected abstract String getEntityNameFilter(T filter);

View File

@ -30,7 +30,7 @@ public class EntityNameQueryProcessor extends AbstractSimpleQueryProcessor<Entit
public EntityNameQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) { public EntityNameQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) {
super(repo, ctx, query, (EntityNameFilter) query.getEntityFilter(), ((EntityNameFilter) query.getEntityFilter()).getEntityType()); super(repo, ctx, query, (EntityNameFilter) query.getEntityFilter(), ((EntityNameFilter) query.getEntityFilter()).getEntityType());
pattern = RepositoryUtils.toSqlLikePattern(filter.getEntityNameFilter()); pattern = RepositoryUtils.toContainsSqlLikePattern(filter.getEntityNameFilter());
} }
@Override @Override

View File

@ -249,11 +249,11 @@ public class RepositoryUtils {
} }
return switch (predicate.getOperation()) { return switch (predicate.getOperation()) {
case EQUAL -> value.equals(predicateValue); case EQUAL -> value.equals(predicateValue);
case STARTS_WITH -> value.startsWith(predicateValue); case STARTS_WITH -> toStartsWithSqlLikePattern(predicateValue).matcher(value).matches();
case ENDS_WITH -> value.endsWith(predicateValue); case ENDS_WITH -> toEndsWithSqlLikePattern(predicateValue).matcher(value).matches();
case NOT_EQUAL -> !value.equals(predicateValue); case NOT_EQUAL -> !value.equals(predicateValue);
case CONTAINS -> value.contains(predicateValue); case CONTAINS -> toContainsSqlLikePattern(predicateValue).matcher(value).matches();
case NOT_CONTAINS -> !value.contains(predicateValue); case NOT_CONTAINS -> !toContainsSqlLikePattern(predicateValue).matcher(value).matches();
case IN -> equalsAny(value, splitByCommaWithoutQuotes(predicateValue)); case IN -> equalsAny(value, splitByCommaWithoutQuotes(predicateValue));
case NOT_IN -> !equalsAny(value, splitByCommaWithoutQuotes(predicateValue)); case NOT_IN -> !equalsAny(value, splitByCommaWithoutQuotes(predicateValue));
}; };
@ -304,6 +304,15 @@ public class RepositoryUtils {
return true; return true;
} else if (filterPredicates.getOperation() == OR) { } else if (filterPredicates.getOperation() == OR) {
for (KeyFilterPredicate filterPredicate : filterPredicates.getPredicates()) { for (KeyFilterPredicate filterPredicate : filterPredicates.getPredicates()) {
// Emulate the SQL-like behavior of ThingsBoard's Entity Data Query service:
// for COMPLEX filters, return no results if filter value is empty
if (filterPredicate instanceof StringFilterPredicate stringFilterPredicate) {
if (StringUtils.isEmpty(stringFilterPredicate.getValue().getValue())) {
continue;
}
}
if (simpleKeyFilter.check(value, filterPredicate)) { if (simpleKeyFilter.check(value, filterPredicate)) {
return true; return true;
} }
@ -314,25 +323,40 @@ public class RepositoryUtils {
} }
} }
public static Pattern toSqlLikePattern(String nameFilter) { public static Pattern toContainsSqlLikePattern(String filter) {
if (StringUtils.isNotBlank(nameFilter)) { if (StringUtils.isNotBlank(filter)) {
boolean percentSymbolOnStart = nameFilter.startsWith("%"); return toSqlLikePattern(filter, ".*", ".*");
boolean percentSymbolOnEnd = nameFilter.endsWith("%");
if (percentSymbolOnStart) {
nameFilter = nameFilter.substring(1);
}
if (percentSymbolOnEnd) {
nameFilter = nameFilter.substring(0, nameFilter.length() - 1);
}
if (percentSymbolOnStart || percentSymbolOnEnd) {
return Pattern.compile((percentSymbolOnStart ? ".*" : "") + Pattern.quote(nameFilter) + (percentSymbolOnEnd ? ".*" : ""), Pattern.CASE_INSENSITIVE);
} else {
return Pattern.compile(Pattern.quote(nameFilter) + ".*", Pattern.CASE_INSENSITIVE);
}
} }
return null; return null;
} }
private static Pattern toStartsWithSqlLikePattern(String filter) {
return toSqlLikePattern(filter, "^", ".*");
}
private static Pattern toEndsWithSqlLikePattern(String filter) {
return toSqlLikePattern(filter, ".*", "$");
}
private static Pattern toSqlLikePattern(String value, String prefix, String suffix ) {
if (value.contains("%") || value.contains("_")) {
String regexValue = value
.replace("_", ".")
.replace("%", ".*");
String regex;
if ("^".equals(prefix)) {
regex = "^" + regexValue + (regexValue.endsWith(".*") ? "" : ".*");
} else if ("$".equals(suffix)) {
regex = (regexValue.startsWith(".*") ? "" : ".*") + regexValue + "$";
} else {
regex = (regexValue.startsWith(".*") ? "" : ".*") + regexValue + (regexValue.endsWith(".*") ? "" : ".*");
}
return Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
} else {
return Pattern.compile(prefix + Pattern.quote(value) + suffix, Pattern.CASE_INSENSITIVE);
}
}
@FunctionalInterface @FunctionalInterface
public interface SimpleKeyFilter<T> { public interface SimpleKeyFilter<T> {

View File

@ -70,7 +70,45 @@ public class RepositoryUtilsTest {
Arguments.of("loranet 123", getNameFilter(StringOperation.IN, "loranet 123, loranet 124"), true), Arguments.of("loranet 123", getNameFilter(StringOperation.IN, "loranet 123, loranet 124"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.IN, "loranet 125, loranet 126"), false), Arguments.of("loranet 123", getNameFilter(StringOperation.IN, "loranet 125, loranet 126"), false),
Arguments.of("loranet 123", getNameFilter(StringOperation.NOT_IN, "loranet 125, loranet 126"), true), Arguments.of("loranet 123", getNameFilter(StringOperation.NOT_IN, "loranet 125, loranet 126"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.NOT_IN, "loranet 123, loranet 126"), false) Arguments.of("loranet 123", getNameFilter(StringOperation.NOT_IN, "loranet 123, loranet 126"), false),
// Basic CONTAINS
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "%loranet"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "loranet%"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "%ranet%"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "%123"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "%loranx%"), false),
// Basic STARTS_WITH
Arguments.of("loranet 123", getNameFilter(StringOperation.STARTS_WITH, "loranet%"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.STARTS_WITH, "lora%"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.STARTS_WITH, "lorax%"), false),
// Basic ENDS_WITH
Arguments.of("loranet 123", getNameFilter(StringOperation.ENDS_WITH, "%123"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.ENDS_WITH, "%23"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.ENDS_WITH, "%124"), false),
// CONTAINS with _
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "loranet_123"), true), // '_' = ' '
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "loranet_12_"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "loran_t%"), true),
// STARTS_WITH with _
Arguments.of("loranet 123", getNameFilter(StringOperation.STARTS_WITH, "loranet_"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.STARTS_WITH, "lora__t%"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.STARTS_WITH, "lor_net%"), true),
// ENDS_WITH with _
Arguments.of("loranet 123", getNameFilter(StringOperation.ENDS_WITH, "_23"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.ENDS_WITH, "_2_"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.ENDS_WITH, "_3"), true),
// Mixed patterns
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "lora__t 1%"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "lora%net%3"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "%o_anet%2_3"), false),
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "lora___ ___"), true)
); );
} }