Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
Expand Up @@ -15,6 +15,7 @@

import com.google.common.collect.Lists;
import jakarta.persistence.criteria.*;
import lombok.extern.slf4j.Slf4j;
import org.gridsuite.computation.dto.ResourceFilterDTO;
import org.jetbrains.annotations.NotNull;
import org.springframework.data.jpa.domain.Specification;
Expand All @@ -32,15 +33,16 @@
*
* @author Kevin Le Saulnier <[email protected]>
*/
@Slf4j
public final class SpecificationUtils {
/**
* Maximum values per IN clause chunk to avoid StackOverflow exceptions.
* Current value (500) is a safe default but can be changed
*/
public static final int MAX_IN_CLAUSE_SIZE = 500;

public static final String FIELD_SEPARATOR = ".";

/**
Prefer less than 1000 for Oracle DB / good compromise for Postgres
*/
public static final int MAX_IN_CLAUSE_SIZE = 10000;

// Utility class, so no constructor
private SpecificationUtils() { }

Expand All @@ -58,6 +60,11 @@ public static <X> Specification<X> notEqual(String field, String value) {
return (root, cq, cb) -> cb.notEqual(getColumnPath(root, field), value);
}

public static <X> Specification<X> in(String field, List<String> values) {
return (root, cq, cb) ->
cb.upper(getColumnPath(root, field).as(String.class)).in(values);
}

public static <X> Specification<X> contains(String field, String value) {
return (root, cq, cb) -> cb.like(cb.upper(getColumnPath(root, field).as(String.class)), "%" + EscapeCharacter.DEFAULT.escape(value).toUpperCase() + "%", EscapeCharacter.DEFAULT.getEscapeCharacter());
}
Expand Down Expand Up @@ -142,15 +149,22 @@ private static <X> Specification<X> appendTextFilterToSpecification(Specificatio
// implicitly an IN resourceFilter type because only IN may have value lists as filter value
List<String> inValues = valueList.stream()
.map(Object::toString)
.map(String::toUpperCase)
.toList();
completedSpecification = completedSpecification.and(
resourceFilter.type() == ResourceFilterDTO.Type.NOT_EQUAL ?
not(generateInSpecification(resourceFilter.column(), inValues)) :
generateInSpecification(resourceFilter.column(), inValues)
);
} else if (resourceFilter.value() == null) {
// if the value is null, we build an impossible specification (trick to remove later on ?)
completedSpecification = completedSpecification.and(not(completedSpecification));
} else {
completedSpecification = completedSpecification.and(equals(resourceFilter.column(), resourceFilter.value().toString()));
completedSpecification = completedSpecification.and(
resourceFilter.type() == ResourceFilterDTO.Type.NOT_EQUAL ?
notEqual(resourceFilter.column(), resourceFilter.value().toString()) :
equals(resourceFilter.column(), resourceFilter.value().toString())
);
}
}
case CONTAINS -> {
Expand Down Expand Up @@ -183,32 +197,17 @@ private static <X> Specification<X> appendTextFilterToSpecification(Specificatio
* @return a specification for the IN clause
*/
private static <X> Specification<X> generateInSpecification(String column, List<String> inPossibleValues) {

if (inPossibleValues.size() > MAX_IN_CLAUSE_SIZE) {
// there are too many values for only one call to anyOf() : it might cause a StackOverflow
// => the specification is divided into several specifications which have an OR between them :
List<List<String>> chunksOfInValues = Lists.partition(inPossibleValues, MAX_IN_CLAUSE_SIZE);
Specification<X> containerSpec = null;
for (List<String> chunk : chunksOfInValues) {
Specification<X> multiOrEqualSpec = anyOf(
chunk
.stream()
.map(value -> SpecificationUtils.<X>equals(column, value))
.toList()
);
if (containerSpec == null) {
containerSpec = multiOrEqualSpec;
} else {
containerSpec = containerSpec.or(multiOrEqualSpec);
}
List<List<String>> chunksOfInValues = Lists.partition(inPossibleValues, MAX_IN_CLAUSE_SIZE);
Specification<X> containerSpec = null;
for (List<String> chunk : chunksOfInValues) {
Specification<X> multiOrEqualSpec = Specification.anyOf(in(column, chunk));
if (containerSpec == null) {
containerSpec = multiOrEqualSpec;
} else {
containerSpec = containerSpec.or(multiOrEqualSpec);
}
return containerSpec;
}
return anyOf(inPossibleValues
.stream()
.map(value -> SpecificationUtils.<X>equals(column, value))
.toList()
);
return containerSpec;
}

@NotNull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ void testBuildSpecification() {
// test data
List<ResourceFilterDTO> resourceFilters = List.of(
new ResourceFilterDTO(TEXT, EQUALS, "dummyColumnValue", "dummyColumn"),
new ResourceFilterDTO(TEXT, NOT_EQUAL, "dummyColumnValue", "dummyColumn"),
new ResourceFilterDTO(TEXT, NOT_EQUAL, List.of("dummyColumnValue", "otherDummyColumnValue"), "dummyColumn"),
new ResourceFilterDTO(TEXT, STARTS_WITH, "dum", "dummyColumn"),
new ResourceFilterDTO(TEXT, IN, List.of("dummyColumnValue", "otherDummyColumnValue"), "dummyColumn"),
new ResourceFilterDTO(TEXT, IN, tooManyInValues, "dummyColumn"),
Expand Down