Skip to content

Commit

Permalink
feat : introduce criteriagroup for searching (#1605)
Browse files Browse the repository at this point in the history
  • Loading branch information
rajadilipkolli authored Dec 28, 2024
1 parent 314c1b9 commit 40f6b30
Show file tree
Hide file tree
Showing 9 changed files with 324 additions and 142 deletions.
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));
} 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,
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 @@ public void setValues(List<String> values) {
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),
criteriaBuilder::and);
case LT -> combinePredicates(
typedValues,
value -> criteriaBuilder.lessThan(root.get(getField()), (Comparable) value),
criteriaBuilder::and);
case GTE -> combinePredicates(
typedValues,
value -> criteriaBuilder.greaterThanOrEqualTo(root.get(getField()), (Comparable) value),
criteriaBuilder::and);
case LTE -> combinePredicates(
typedValues,
value -> criteriaBuilder.lessThanOrEqualTo(root.get(getField()), (Comparable) value),
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(
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);
} 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)));
}

@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

0 comments on commit 40f6b30

Please sign in to comment.