-
Notifications
You must be signed in to change notification settings - Fork 9
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
feat : introduce criteriagroup for searching #1605
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package com.example.keysetpagination.model.query; | ||
|
||
import java.util.List; | ||
import org.springframework.data.jpa.domain.Specification; | ||
|
||
public class CriteriaGroup implements ISearchCriteria { | ||
|
||
private QueryOperator operator; // AND or OR | ||
private List<ISearchCriteria> criteriaList; | ||
|
||
public CriteriaGroup() {} | ||
|
||
public CriteriaGroup(QueryOperator operator, List<ISearchCriteria> criteriaList) { | ||
this.operator = operator; | ||
this.criteriaList = criteriaList; | ||
} | ||
|
||
public QueryOperator getOperator() { | ||
return operator; | ||
} | ||
|
||
public CriteriaGroup setOperator(QueryOperator operator) { | ||
this.operator = operator; | ||
return this; | ||
} | ||
|
||
public List<ISearchCriteria> getCriteriaList() { | ||
return criteriaList; | ||
} | ||
|
||
public CriteriaGroup setCriteriaList(List<ISearchCriteria> criteriaList) { | ||
this.criteriaList = criteriaList; | ||
return this; | ||
} | ||
|
||
@Override | ||
public Specification<?> toSpecification() { | ||
List<Specification> specs = | ||
criteriaList.stream().map(ISearchCriteria::toSpecification).toList(); | ||
|
||
Specification result = specs.getFirst(); | ||
for (int i = 1; i < specs.size(); i++) { | ||
if (getOperator() == QueryOperator.AND) { | ||
result = result.and(specs.get(i)); | ||
Check warning on line 44 in jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/model/query/CriteriaGroup.java GitHub Actions / Qodana Community for JVMUnchecked warning
|
||
} else if (getOperator() == QueryOperator.OR) { | ||
result = result.or(specs.get(i)); | ||
Check warning on line 46 in jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/model/query/CriteriaGroup.java GitHub Actions / Qodana Community for JVMUnchecked warning
|
||
} else { | ||
throw new UnsupportedOperationException("Operator not supported in group: " + operator); | ||
} | ||
} | ||
return result; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package com.example.keysetpagination.model.query; | ||
|
||
import com.fasterxml.jackson.annotation.JsonSubTypes; | ||
import com.fasterxml.jackson.annotation.JsonTypeInfo; | ||
import org.springframework.data.jpa.domain.Specification; | ||
|
||
@JsonTypeInfo( | ||
use = JsonTypeInfo.Id.NAME, | ||
include = JsonTypeInfo.As.PROPERTY, | ||
Check warning on line 9 in jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/model/query/ISearchCriteria.java GitHub Actions / Qodana Community for JVMDefault annotation parameter value
|
||
property = "type", | ||
defaultImpl = SearchCriteria.class // Set default implementation | ||
) | ||
@JsonSubTypes({ | ||
@JsonSubTypes.Type(value = SearchCriteria.class, name = "criteria"), | ||
@JsonSubTypes.Type(value = CriteriaGroup.class, name = "group") | ||
}) | ||
public interface ISearchCriteria<T> { | ||
|
||
Specification<T> toSpecification(); | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,13 +1,23 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
package com.example.keysetpagination.model.query; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
import jakarta.persistence.criteria.Predicate; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import jakarta.validation.Valid; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import jakarta.validation.constraints.NotBlank; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import jakarta.validation.constraints.NotNull; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import jakarta.validation.constraints.Size; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import java.math.BigDecimal; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import java.time.LocalDate; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import java.time.LocalDateTime; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import java.time.format.DateTimeFormatter; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import java.util.List; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import java.util.StringJoiner; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import java.util.UUID; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import java.util.function.BiFunction; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import java.util.function.Function; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import org.springframework.data.jpa.domain.Specification; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
import org.springframework.util.CollectionUtils; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
public class SearchCriteria { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
public class SearchCriteria<T> implements ISearchCriteria<T> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
@NotNull(message = "Operator cannot be null") private QueryOperator queryOperator; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -50,6 +60,126 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||
this.values = values; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
@Override | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
public Specification<T> toSpecification() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
return (root, query, criteriaBuilder) -> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Implement predicate creation logic based on queryOperator | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Predicate predicate = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
if ((CollectionUtils.isEmpty(values)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
&& getQueryOperator() != QueryOperator.IN | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
&& getQueryOperator() != QueryOperator.NOT_IN) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
throw new IllegalArgumentException( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
"Values cannot be null or empty for operator: " + getQueryOperator()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Fetch the field type | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Class<?> fieldType = root.get(getField()).getJavaType(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Convert values to the appropriate type | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
List<Object> typedValues = values.stream() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
.map(value -> convertToType(value, fieldType)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
.toList(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Switch for building predicates | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
return switch (getQueryOperator()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
case EQ -> combinePredicates( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
typedValues, value -> criteriaBuilder.equal(root.get(getField()), value), criteriaBuilder::and); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
case NE -> combinePredicates( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
typedValues, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
value -> criteriaBuilder.notEqual(root.get(getField()), value), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
criteriaBuilder::and); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
case GT -> combinePredicates( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
typedValues, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
value -> criteriaBuilder.greaterThan(root.get(getField()), (Comparable) value), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Check warning on line 94 in jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/model/query/SearchCriteria.java GitHub Actions / Qodana Community for JVMUnchecked warning
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
criteriaBuilder::and); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
case LT -> combinePredicates( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
typedValues, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
value -> criteriaBuilder.lessThan(root.get(getField()), (Comparable) value), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Check warning on line 98 in jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/model/query/SearchCriteria.java GitHub Actions / Qodana Community for JVMUnchecked warning
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
criteriaBuilder::and); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
case GTE -> combinePredicates( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
typedValues, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
value -> criteriaBuilder.greaterThanOrEqualTo(root.get(getField()), (Comparable) value), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Check warning on line 102 in jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/model/query/SearchCriteria.java GitHub Actions / Qodana Community for JVMUnchecked warning
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
criteriaBuilder::and); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
case LTE -> combinePredicates( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
typedValues, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
value -> criteriaBuilder.lessThanOrEqualTo(root.get(getField()), (Comparable) value), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Check warning on line 106 in jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/model/query/SearchCriteria.java GitHub Actions / Qodana Community for JVMUnchecked warning
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
criteriaBuilder::and); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
case LIKE, CONTAINS -> combinePredicates( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
typedValues, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
value -> criteriaBuilder.like( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
criteriaBuilder.lower(root.get(getField())), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
"%" + value.toString().toLowerCase() + "%"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
criteriaBuilder::or); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
case STARTS_WITH -> combinePredicates( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
typedValues, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
value -> criteriaBuilder.like(root.get(getField()), value + "%"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
criteriaBuilder::and); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
case ENDS_WITH -> combinePredicates( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
typedValues, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
value -> criteriaBuilder.like(root.get(getField()), "%" + value), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
criteriaBuilder::and); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
case BETWEEN -> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (typedValues.size() != 2) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
throw new IllegalArgumentException("BETWEEN operator requires exactly two values"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
yield criteriaBuilder.between( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Check warning on line 126 in jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/model/query/SearchCriteria.java GitHub Actions / Qodana Community for JVMUnchecked warning
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
root.get(getField()), (Comparable) typedValues.get(0), (Comparable) typedValues.get(1)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
case IN -> root.get(getField()).in(typedValues); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
case NOT_IN -> criteriaBuilder.not(root.get(getField()).in(typedValues)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
case OR -> criteriaBuilder.or(root.get(getField()).in(typedValues)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
case AND -> criteriaBuilder.and(root.get(getField()).in(typedValues)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
default -> throw new IllegalArgumentException("Unsupported operator: " + getQueryOperator()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
private Object convertToType(String value, Class<?> fieldType) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (fieldType.equals(String.class)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
return value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} else if (fieldType.equals(BigDecimal.class)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
return new BigDecimal(value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} else if (fieldType.equals(UUID.class)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
return UUID.fromString(value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} else if (fieldType.equals(Integer.class) || fieldType.equals(int.class)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
return Integer.valueOf(value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} else if (fieldType.equals(Long.class) || fieldType.equals(long.class)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
return Long.valueOf(value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} else if (fieldType.equals(Double.class) || fieldType.equals(double.class)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
return Double.valueOf(value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} else if (fieldType.equals(Boolean.class) || fieldType.equals(boolean.class)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
return Boolean.valueOf(value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} else if (fieldType.equals(LocalDate.class)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
return LocalDate.parse(value, DateTimeFormatter.ISO_DATE); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} else if (fieldType.equals(LocalDateTime.class)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
return LocalDateTime.parse(value, DateTimeFormatter.ISO_DATE_TIME); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} else if (Enum.class.isAssignableFrom(fieldType)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
return Enum.valueOf((Class<Enum>) fieldType, value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Check warning on line 159 in jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/model/query/SearchCriteria.java GitHub Actions / Qodana Community for JVMUnchecked warning
Check warning on line 159 in jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/model/query/SearchCriteria.java GitHub Actions / Qodana Community for JVMUnchecked warning
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
throw new IllegalArgumentException("Unsupported field type: " + fieldType.getName()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} catch (Exception e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
throw new IllegalArgumentException( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
"Failed to convert value '" + value + "' to type " + fieldType.getName(), e); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
private Predicate combinePredicates( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
List<Object> values, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Function<Object, Predicate> predicateFunction, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
BiFunction<Predicate, Predicate, Predicate> combiner) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (values.size() == 1) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
return predicateFunction.apply(values.getFirst()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
return values.stream() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
.map(predicateFunction) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
.reduce(combiner::apply) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
.orElseThrow(() -> new IllegalArgumentException( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
String.format("No predicates could be generated from values: %s", values))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+169
to
+182
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Likely bug in single-value predicate creation. - return predicateFunction.apply(values.getFirst());
+ return predicateFunction.apply(values.get(0)); 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
@Override | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
public String toString() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
return new StringJoiner(", ", SearchCriteria.class.getSimpleName() + "[", "]") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prevent possible
IndexOutOfBoundsException
inspecs.getFirst()
.criteriaList
is empty, thenspecs
is empty too, andspecs.getFirst()
is not a standard JavaList
method unless you rely on a specific library extension. Double-check the utility used here.specs
is from a standardList
, you probably needspecs.get(0)
instead.📝 Committable suggestion