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

feat : introduce criteriagroup for searching #1605

Merged
merged 1 commit into from
Dec 28, 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
2 changes: 1 addition & 1 deletion jpa/keyset-pagination/boot-data-window-pagination/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.1</version>
<version>3.4.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example.keysetpagination</groupId>
Expand Down
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

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unchecked warning

Unchecked call to 'and(Specification)' as a member of raw type 'org.springframework.data.jpa.domain.Specification'
} 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

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unchecked warning

Unchecked call to 'or(Specification)' as a member of raw type 'org.springframework.data.jpa.domain.Specification'
} else {
throw new UnsupportedOperationException("Operator not supported in group: " + operator);
}
}
return result;
}
Comment on lines +36 to +52
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Prevent possible IndexOutOfBoundsException in specs.getFirst().

  1. If criteriaList is empty, then specs is empty too, and specs.getFirst() is not a standard Java List method unless you rely on a specific library extension. Double-check the utility used here.
  2. If specs is from a standard List, you probably need specs.get(0) instead.
  3. Consider returning a no-op specification when empty instead of throwing a runtime error.
- Specification result = specs.getFirst();
+ if (specs.isEmpty()) {
+     return (root, query, cb) -> cb.conjunction();
+ }
+ Specification result = specs.get(0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@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));
} else if (getOperator() == QueryOperator.OR) {
result = result.or(specs.get(i));
} else {
throw new UnsupportedOperationException("Operator not supported in group: " + operator);
}
}
return result;
}
@Override
public Specification<?> toSpecification() {
List<Specification> specs =
criteriaList.stream().map(ISearchCriteria::toSpecification).toList();
if (specs.isEmpty()) {
return (root, query, cb) -> cb.conjunction();
}
Specification result = specs.get(0);
for (int i = 1; i < specs.size(); i++) {
if (getOperator() == QueryOperator.AND) {
result = result.and(specs.get(i));
} else if (getOperator() == QueryOperator.OR) {
result = result.or(specs.get(i));
} 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

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Default annotation parameter value

Redundant default parameter value assignment
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;

Expand Down Expand Up @@ -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

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unchecked warning

Unchecked method 'greaterThan(Expression, Y)' invocation
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

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unchecked warning

Unchecked method 'lessThan(Expression, Y)' invocation
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

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unchecked warning

Unchecked method 'greaterThanOrEqualTo(Expression, Y)' invocation
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

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unchecked warning

Unchecked method 'lessThanOrEqualTo(Expression, Y)' invocation
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

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unchecked warning

Unchecked method 'between(Expression, Y, Y)' invocation
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

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unchecked warning

Unchecked cast: 'java.lang.Class\>' to 'java.lang.Class'

Check warning on line 159 in jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/model/query/SearchCriteria.java

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unchecked warning

Unchecked method 'valueOf(Class, String)' invocation
} 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Likely bug in single-value predicate creation.
Line 174 uses values.getFirst(), but List in Java has no getFirst() method. This should be values.get(0) unless a custom List implementation provides it. This can cause a runtime error.

- return predicateFunction.apply(values.getFirst());
+ return predicateFunction.apply(values.get(0));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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)));
}
private Predicate combinePredicates(
List<Object> values,
Function<Object, Predicate> predicateFunction,
BiFunction<Predicate, Predicate, Predicate> combiner) {
if (values.size() == 1) {
return predicateFunction.apply(values.get(0));
}
return values.stream()
.map(predicateFunction)
.reduce(combiner::apply)
.orElseThrow(() -> new IllegalArgumentException(
String.format("No predicates could be generated from values: %s", values)));
}

@Override
public String toString() {
return new StringJoiner(", ", SearchCriteria.class.getSimpleName() + "[", "]")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,24 @@

public class SearchRequest {

private List<SearchCriteria> searchCriteriaList;

private List<ISearchCriteria> searchCriteriaList;
private List<SortRequest> sortRequests;

public SearchRequest() {
this.searchCriteriaList = new ArrayList<>();
this.sortRequests = new ArrayList<>();
}

public SearchRequest(List<SearchCriteria> searchCriteriaList, List<SortRequest> sortRequests) {
public SearchRequest(List<ISearchCriteria> searchCriteriaList, List<SortRequest> sortRequests) {
this.searchCriteriaList = searchCriteriaList != null ? searchCriteriaList : new ArrayList<>();
this.sortRequests = sortRequests != null ? sortRequests : new ArrayList<>();
}

public List<SearchCriteria> getSearchCriteriaList() {
public List<ISearchCriteria> getSearchCriteriaList() {
return searchCriteriaList;
}

public SearchRequest setSearchCriteriaList(List<SearchCriteria> searchCriteriaList) {
public SearchRequest setSearchCriteriaList(List<ISearchCriteria> searchCriteriaList) {
this.searchCriteriaList = searchCriteriaList;
return this;
}
Expand Down
Loading
Loading