Skip to content

Commit

Permalink
implement code review comments
Browse files Browse the repository at this point in the history
  • Loading branch information
rajadilipkolli committed Dec 3, 2024
1 parent cc31971 commit aae7d5b
Show file tree
Hide file tree
Showing 9 changed files with 530 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -1,19 +1,66 @@
package com.example.keysetpagination.model.query;

/**
* Represents the supported operators for dynamic query specifications.
* These operators are used to build dynamic JPA queries through specifications.
*/
public enum QueryOperator {

// Comparison Operators
/** Equal to */
EQ,
/** Not equal to */
NE,
/** Less than */
LT,
/** Greater than */
GT,
/** Greater than or equal to */
GTE,
/** Less than or equal to */
LTE,
/** Between two values (inclusive) */
BETWEEN,
/** Is null check */
IS_NULL,
/** Is not null check */
IS_NOT_NULL,

// Collection Operators
/** In a collection of values */
IN,
/** Not in a collection of values */
NOT_IN,

// String Operators
/** SQL LIKE operation */
LIKE,
/** Contains substring */
CONTAINS,
/** Starts with prefix */
STARTS_WITH,
/** Ends with suffix */
ENDS_WITH,

// Logical Operators
/** Logical AND */
AND,
OR,
NOTIN
/** Logical OR */
OR;

/**
* Checks if the operator is applicable to string fields.
* @return true if the operator can be used with strings
*/
public boolean isStringOperator() {
return this == LIKE || this == CONTAINS || this == STARTS_WITH || this == ENDS_WITH;
}

/**
* Checks if the operator is a logical operator.
* @return true if the operator is logical (AND, OR)
*/
public boolean isLogicalOperator() {
return this == AND || this == OR;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.example.keysetpagination.model.query;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
Expand All @@ -13,11 +16,12 @@
@NoArgsConstructor
public class SearchCriteria {

@NotBlank(message = "Operator cannot be null")
private QueryOperator queryOperator;
@NotNull(message = "Operator cannot be null") private QueryOperator queryOperator;

@NotBlank(message = "Field name cannot be null or blank")
private String field;

@NotNull(message = "Values list cannot be null") @Size(min = 1, message = "Values list cannot be empty")
@Valid
private List<String> values;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
import org.springframework.data.jpa.domain.Specification;

/**
* Custom repository interface for efficient keyset pagination for any entities.
* Custom repository interface for efficient keyset pagination.
*
* @param <T> the domain type the repository manages
* @see org.springframework.data.domain.Window
*/
public interface CustomRepository<T> {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.example.keysetpagination.repositories;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.*;
import java.util.ArrayList;
Expand All @@ -17,7 +16,6 @@
@Repository
public class CustomRepositoryImpl<T> implements CustomRepository<T> {

@PersistenceContext
private final EntityManager entityManager;

public CustomRepositoryImpl(EntityManager entityManager) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ private Pageable createPageable(FindAnimalsQuery findAnimalsQuery) {
return PageRequest.of(pageNo, findAnimalsQuery.pageSize(), sort);
}

public Window<AnimalResponse> searchAnimals(SearchCriteria[] searchCriteriaArray, int pageSize, Long scrollId) {
public Window<AnimalResponse> searchAnimals(List<SearchCriteria> searchCriteriaList, int pageSize, Long scrollId) {

Specification<Animal> specification =
animalEntitySpecification.specificationBuilder(searchCriteriaArray, Animal.class);
animalEntitySpecification.specificationBuilder(searchCriteriaList, Animal.class);

// Create initial ScrollPosition or continue from the given scrollId
ScrollPosition position = scrollId == null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
import jakarta.persistence.criteria.Root;
import jakarta.persistence.metamodel.ManagedType;
import jakarta.persistence.metamodel.Metamodel;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Stream;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

@Service
public class EntitySpecification<T> {
Expand All @@ -24,11 +26,11 @@ public EntitySpecification(EntityManager entityManager) {
this.entityManager = entityManager;
}

public Specification<T> specificationBuilder(SearchCriteria[] searchCriteriaArray, Class<T> entityClass) {
validateMetadata(searchCriteriaArray, entityClass);
public Specification<T> specificationBuilder(List<SearchCriteria> searchCriteriaList, Class<T> entityClass) {
validateMetadata(searchCriteriaList, entityClass);
return (root, query, criteriaBuilder) -> {
// Dynamically build predicates based on filters
return Stream.of(searchCriteriaArray)
return searchCriteriaList.stream()
.map(entry -> createPredicate(entry, root, criteriaBuilder))
.reduce(criteriaBuilder::and)
.orElse(criteriaBuilder.conjunction());
Expand All @@ -40,61 +42,104 @@ private Predicate createPredicate(SearchCriteria searchCriteria, Root<T> root, C
QueryOperator operator = searchCriteria.getQueryOperator();
List<String> values = searchCriteria.getValues();

if (values == null || values.isEmpty() && operator != QueryOperator.IN && operator != QueryOperator.NOTIN) {
if ((CollectionUtils.isEmpty(values)) && operator != QueryOperator.IN && operator != QueryOperator.NOT_IN) {
throw new IllegalArgumentException("Values cannot be null or empty for operator: " + operator);
}

// Fetch the field type
Class<?> fieldType = root.get(fieldName).getJavaType();

// Convert values to the appropriate type
List<Object> typedValues =
values.stream().map(value -> convertToType(value, fieldType)).toList();

// Switch for building predicates
return switch (operator) {
case EQ -> combinePredicates(
values, value -> criteriaBuilder.equal(root.get(fieldName), value), criteriaBuilder::and);
typedValues, value -> criteriaBuilder.equal(root.get(fieldName), value), criteriaBuilder::and);
case NE -> combinePredicates(
values, value -> criteriaBuilder.notEqual(root.get(fieldName), value), criteriaBuilder::and);
typedValues, value -> criteriaBuilder.notEqual(root.get(fieldName), value), criteriaBuilder::and);
case GT -> combinePredicates(
values, value -> criteriaBuilder.greaterThan(root.get(fieldName), value), criteriaBuilder::and);
typedValues,
value -> criteriaBuilder.greaterThan(root.get(fieldName), (Comparable) value),
criteriaBuilder::and);
case LT -> combinePredicates(
values, value -> criteriaBuilder.lessThan(root.get(fieldName), value), criteriaBuilder::and);
typedValues,
value -> criteriaBuilder.lessThan(root.get(fieldName), (Comparable) value),
criteriaBuilder::and);
case GTE -> combinePredicates(
values,
value -> criteriaBuilder.greaterThanOrEqualTo(root.get(fieldName), value),
typedValues,
value -> criteriaBuilder.greaterThanOrEqualTo(root.get(fieldName), (Comparable) value),
criteriaBuilder::and);
case LTE -> combinePredicates(
values,
value -> criteriaBuilder.lessThanOrEqualTo(root.get(fieldName), value),
typedValues,
value -> criteriaBuilder.lessThanOrEqualTo(root.get(fieldName), (Comparable) value),
criteriaBuilder::and);
case LIKE, CONTAINS -> combinePredicates(
values, value -> criteriaBuilder.like(root.get(fieldName), "%" + value + "%"), criteriaBuilder::or);
typedValues,
value -> criteriaBuilder.like(root.get(fieldName), "%" + value + "%"),
criteriaBuilder::or);
case STARTS_WITH -> combinePredicates(
values, value -> criteriaBuilder.like(root.get(fieldName), value + "%"), criteriaBuilder::and);
typedValues, value -> criteriaBuilder.like(root.get(fieldName), value + "%"), criteriaBuilder::and);
case ENDS_WITH -> combinePredicates(
values, value -> criteriaBuilder.like(root.get(fieldName), "%" + value), criteriaBuilder::and);
typedValues, value -> criteriaBuilder.like(root.get(fieldName), "%" + value), criteriaBuilder::and);
case BETWEEN -> {
if (values.size() != 2) {
if (typedValues.size() != 2) {
throw new IllegalArgumentException("BETWEEN operator requires exactly two values");
}
yield criteriaBuilder.between(root.get(fieldName), values.get(0), values.get(1));
yield criteriaBuilder.between(
root.get(fieldName), (Comparable) typedValues.get(0), (Comparable) typedValues.get(1));
}
case IN -> root.get(fieldName).in(values);
case NOTIN -> criteriaBuilder.not(root.get(fieldName).in(values));
case IN -> root.get(fieldName).in(typedValues);
case NOT_IN -> criteriaBuilder.not(root.get(fieldName).in(typedValues));
case OR -> criteriaBuilder.or(root.get(fieldName).in(typedValues));
case AND -> criteriaBuilder.and(root.get(fieldName).in(typedValues));
default -> throw new IllegalArgumentException("Unsupported operator: " + operator);
};
}

private Object convertToType(String value, Class<?> fieldType) {
try {
if (fieldType.equals(String.class)) {
return 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);
} else if (fieldType.equals(LocalDateTime.class)) {
return LocalDateTime.parse(value);
} 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<String> values,
Function<String, Predicate> predicateFunction,
List<Object> values,
Function<Object, Predicate> predicateFunction,
BiFunction<Predicate, Predicate, Predicate> combiner) {
return values.stream()
.map(predicateFunction)
.reduce(combiner::apply)
.orElseThrow(() -> new IllegalArgumentException("No predicates could be generated from values"));
}

private void validateMetadata(SearchCriteria[] searchCriteriaArray, Class<T> entityClass) {
private void validateMetadata(List<SearchCriteria> searchCriteriaList, Class<T> entityClass) {
Metamodel metamodel = entityManager.getMetamodel();
ManagedType<T> managedType = metamodel.managedType(entityClass);

for (SearchCriteria searchCriteria : searchCriteriaArray) {
for (SearchCriteria searchCriteria : searchCriteriaList) {
String fieldName = searchCriteria.getField();
if (managedType.getAttribute(fieldName) == null) {
throw new IllegalArgumentException("Invalid field: " + fieldName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import java.net.URI;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Window;
import org.springframework.http.ResponseEntity;
Expand Down Expand Up @@ -68,7 +69,7 @@ public Window<AnimalResponse> searchAnimals(
@Max(100)
int pageSize,
@Parameter(description = "Scroll ID for pagination") @RequestParam(required = false) Long scrollId,
@RequestBody SearchCriteria[] searchCriteria) {
@RequestBody @Valid List<SearchCriteria> searchCriteria) {

return animalService.searchAnimals(searchCriteria, pageSize, scrollId);
}
Expand Down
Loading

0 comments on commit aae7d5b

Please sign in to comment.