From fd27401de4d1946036a2654a21bb68b91ad856db Mon Sep 17 00:00:00 2001 From: Arnold Galovics Date: Tue, 23 May 2023 21:17:22 +0200 Subject: [PATCH] FINERACT-1724: Final touches for Client V2 search API --- .../core/jpa/CriteriaQueryFactory.java | 70 ++++++++++++++++++- .../client/domain/search/SearchedClient.java | 2 +- .../search/SearchingClientRepositoryImpl.java | 38 +++++----- .../search/domain/ClientSearchData.java | 2 +- .../client/ClientSearchTest.java | 25 ++++++- .../integrationtests/common/ClientHelper.java | 10 +++ 6 files changed, 125 insertions(+), 22 deletions(-) diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/jpa/CriteriaQueryFactory.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/jpa/CriteriaQueryFactory.java index c617b7364..f95680b74 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/jpa/CriteriaQueryFactory.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/jpa/CriteriaQueryFactory.java @@ -19,23 +19,35 @@ package org.apache.fineract.infrastructure.core.jpa; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.function.Supplier; +import javax.persistence.EntityManager; +import javax.persistence.TypedQuery; import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Order; +import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.support.PageableExecutionUtils; import org.springframework.stereotype.Component; @Component +@RequiredArgsConstructor public class CriteriaQueryFactory { - public List fromPageable(Pageable pageable, CriteriaBuilder cb, Root root) { - return fromPageable(pageable, cb, root, () -> null); + private final EntityManager entityManager; + + public List ordersFromPageable(Pageable pageable, CriteriaBuilder cb, Root root) { + return ordersFromPageable(pageable, cb, root, () -> null); } - public List fromPageable(Pageable pageable, CriteriaBuilder cb, Root root, Supplier defaultOrderSupplier) { + public List ordersFromPageable(Pageable pageable, CriteriaBuilder cb, Root root, Supplier defaultOrderSupplier) { List orders = new ArrayList<>(); Sort sort = pageable.getSort(); if (sort.isSorted()) { @@ -54,4 +66,56 @@ public class CriteriaQueryFactory { } return orders; } + + public Page readPage(TypedQuery query, Class domainClass, Pageable pageable, Specification spec) { + + if (pageable.isPaged()) { + query.setFirstResult((int) pageable.getOffset()); + query.setMaxResults(pageable.getPageSize()); + } + + return PageableExecutionUtils.getPage(query.getResultList(), pageable, () -> executeCountQuery(getCountQuery(spec, domainClass))); + } + + private static long executeCountQuery(TypedQuery query) { + List totals = query.getResultList(); + long total = 0L; + + for (Long element : totals) { + total += element == null ? 0 : element; + } + return total; + } + + private TypedQuery getCountQuery(Specification spec, Class domainClass) { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaQuery query = builder.createQuery(Long.class); + + Root root = query.from(domainClass); + + applySpecificationToCriteria(root, spec, query); + + if (query.isDistinct()) { + query.select(builder.countDistinct(root)); + } else { + query.select(builder.count(root)); + } + + // Remove all Orders the Specifications might have applied + query.orderBy(Collections.emptyList()); + + return entityManager.createQuery(query); + } + + public Root applySpecificationToCriteria(Root root, Specification spec, CriteriaQuery query) { + + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + Predicate predicate = spec.toPredicate(root, query, builder); + + if (predicate != null) { + query.where(predicate); + } + + return root; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchedClient.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchedClient.java index 93da80ce9..127ed8b45 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchedClient.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchedClient.java @@ -31,7 +31,7 @@ public class SearchedClient { private final Long id; private final String displayName; private final ExternalId externalId; - private final String accountNo; + private final String accountNumber; private final Long officeId; private final String officeName; private final String mobileNo; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchingClientRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchingClientRepositoryImpl.java index a6d6d3a42..8e6ec4d26 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchingClientRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchingClientRepositoryImpl.java @@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.client.domain.search; import java.util.ArrayList; import java.util.List; import javax.persistence.EntityManager; +import javax.persistence.TypedQuery; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Order; @@ -32,8 +33,8 @@ import org.apache.fineract.infrastructure.core.jpa.CriteriaQueryFactory; import org.apache.fineract.organisation.office.domain.Office; import org.apache.fineract.portfolio.client.domain.Client; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Repository; @Repository @@ -55,28 +56,33 @@ public class SearchingClientRepositoryImpl implements SearchingClientRepository CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(SearchedClient.class); + Root root = query.from(Client.class); Path office = root.get("office"); + Specification spec = (r, q, builder) -> { + Path o = r.get("office"); + + List predicates = new ArrayList<>(); + predicates.add(cb.like(o.get("hierarchy"), hierarchyLikeValue)); + + String searchLikeValue = "%" + searchText + "%"; + predicates.add(cb.or(cb.like(r.get("accountNumber"), searchLikeValue), cb.like(r.get("displayName"), searchLikeValue), + cb.like(r.get("externalId"), searchLikeValue), cb.like(r.get("mobileNo"), searchLikeValue))); + + return cb.and(predicates.toArray(new Predicate[0])); + }; + criteriaQueryFactory.applySpecificationToCriteria(root, spec, query); + + List orders = criteriaQueryFactory.ordersFromPageable(pageable, cb, root, () -> cb.desc(root.get("id"))); + query.orderBy(orders); + query.select(cb.construct(SearchedClient.class, root.get("id"), root.get("displayName"), root.get("externalId"), root.get("accountNumber"), office.get("id"), office.get("name"), root.get("mobileNo"), root.get("status"), root.get("activationDate"), root.get("createdDate"))); - List predicates = new ArrayList<>(); - predicates.add(cb.like(office.get("hierarchy"), hierarchyLikeValue)); + TypedQuery queryToExecute = entityManager.createQuery(query); - String searchLikeValue = "%" + searchText + "%"; - predicates.add(cb.or(cb.like(root.get("accountNumber"), searchLikeValue), cb.like(root.get("displayName"), searchLikeValue), - cb.like(root.get("externalId"), searchLikeValue), cb.like(root.get("mobileNo"), searchLikeValue))); - - query.where(cb.and(predicates.toArray(new Predicate[0]))); - - List orders = criteriaQueryFactory.fromPageable(pageable, cb, root, () -> cb.desc(root.get("id"))); - query.orderBy(orders); - - List result = entityManager.createQuery(query).setFirstResult(pageable.getPageNumber()) - .setMaxResults(pageable.getPageSize()).getResultList(); - - return new PageImpl<>(result, pageable, result.size()); + return criteriaQueryFactory.readPage(queryToExecute, Client.class, pageable, spec); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/domain/ClientSearchData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/domain/ClientSearchData.java index dac1637bc..300d8d7e8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/domain/ClientSearchData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/domain/ClientSearchData.java @@ -30,7 +30,7 @@ public class ClientSearchData { private Long id; private String displayName; private ExternalId externalId; - private String accountNo; + private String accountNumber; private Long officeId; private String officeName; private String mobileNo; diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/ClientSearchTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/ClientSearchTest.java index 8a9c982c0..e37908f7d 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/ClientSearchTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/ClientSearchTest.java @@ -50,6 +50,29 @@ public class ClientSearchTest { clientHelper = new ClientHelper(requestSpec, responseSpec); } + @Test + public void testClientSearchWorks_WithLastnameText_WithPaging() { + // given + String lastname = Utils.randomStringGenerator("Client_LastName_", 5); + PostClientsRequest request1 = ClientHelper.defaultClientCreationRequest(); + request1.setLastname(lastname); + clientHelper.createClient(request1); + + PostClientsRequest request2 = ClientHelper.defaultClientCreationRequest(); + request2.setLastname(lastname); + clientHelper.createClient(request2); + + PostClientsRequest request3 = ClientHelper.defaultClientCreationRequest(); + request3.setLastname(lastname); + clientHelper.createClient(request3); + // when + PageClientSearchData result = clientHelper.searchClients(lastname, 0, 1); + // then + assertThat(result.getTotalElements()).isEqualTo(3); + assertThat(result.getNumberOfElements()).isEqualTo(1); + assertThat(result.getTotalPages()).isEqualTo(3); + } + @Test public void testClientSearchWorks_WithLastnameTextOnDefaultOrdering() { // given @@ -135,7 +158,7 @@ public class ClientSearchTest { PageClientSearchData result = clientHelper.searchClients(client2Data.getAccountNo()); // then assertThat(result.getTotalElements()).isEqualTo(1); - assertThat(result.getContent().get(0).getAccountNo()).isEqualTo(client2Data.getAccountNo()); + assertThat(result.getContent().get(0).getAccountNumber()).isEqualTo(client2Data.getAccountNo()); } @Test diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java index 3ed19480c..0d8ee89f6 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java @@ -109,6 +109,16 @@ public class ClientHelper extends IntegrationTest { return searchClients(request); } + public PageClientSearchData searchClients(String text, int page, int pageSize) { + ClientTextSearch clientTextSearch = new ClientTextSearch(); + clientTextSearch.setText(text); + PagedRequestClientTextSearch request = new PagedRequestClientTextSearch(); + request.setRequest(clientTextSearch); + request.setPage(page); + request.setSize(pageSize); + return searchClients(request); + } + public PageClientSearchData searchClients(String text, SortOrder sortOrder) { ClientTextSearch clientTextSearch = new ClientTextSearch(); clientTextSearch.setText(text);