From 5b6f334379d46f3ce687c41804dba1d51b58993d Mon Sep 17 00:00:00 2001 From: Arnold Galovics Date: Wed, 10 May 2023 15:09:50 +0200 Subject: [PATCH] FINERACT-1724: Limiting query result for search API to prevent OOMs and support for loanId parameter for retrieving loan reschedule requests --- .../api/RescheduleLoansApiResource.java | 25 +-- ...nRescheduleRequestReadPlatformService.java | 2 +- ...cheduleRequestReadPlatformServiceImpl.java | 25 ++- .../SearchReadPlatformServiceImpl.java | 163 ++++++++---------- 4 files changed, 99 insertions(+), 116 deletions(-) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/api/RescheduleLoansApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/api/RescheduleLoansApiResource.java index b2232bcc9..4e1a1adea 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/api/RescheduleLoansApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/api/RescheduleLoansApiResource.java @@ -198,7 +198,8 @@ public class RescheduleLoansApiResource { @Operation(summary = "Retrieve all reschedule requests", description = "Retrieve all reschedule requests.") @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = RescheduleLoansApiResourceSwagger.GetLoanRescheduleRequestResponse.class)))) }) - public String retrieveAllRescheduleRequest(@Context final UriInfo uriInfo, @QueryParam("command") final String command) { + public String retrieveAllRescheduleRequest(@Context final UriInfo uriInfo, @QueryParam("command") final String command, + @QueryParam("loanId") Long loanId) { this.platformSecurityContext.authenticatedUser().validateHasReadPermission(RescheduleLoansApiConstants.ENTITY_NAME); @@ -209,28 +210,8 @@ public class RescheduleLoansApiResource { RescheduleLoansApiConstants.rejectCommandParamName); } final List loanRescheduleRequestsData = this.loanRescheduleRequestReadPlatformService - .retrieveAllRescheduleRequests(command); + .retrieveAllRescheduleRequests(command, loanId); return this.loanRescheduleRequestToApiJsonSerializer.serialize(settings, loanRescheduleRequestsData); } - - /* - * @GET - * - * @Path("{scheduleId}") - * - * @Consumes({ MediaType.APPLICATION_JSON }) - * - * @Produces({ MediaType.APPLICATION_JSON }) public String retrieveTemplate(@Context final UriInfo uriInfo) { - * - * this.platformSecurityContext.authenticatedUser(). - * validateHasReadPermission(RescheduleLoansApiConstants.ENTITY_NAME); final ApiRequestJsonSerializationSettings - * settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - * - * LoanRescheduleRequestData loanRescheduleReasons = null; loanRescheduleReasons = - * this.loanRescheduleRequestReadPlatformService .retrieveAllRescheduleReasons(RescheduleLoansApiConstants. - * LOAN_RESCHEDULE_REASON); - * - * return this.loanRescheduleRequestToApiJsonSerializer.serialize(settings, loanRescheduleReasons); } - */ } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestReadPlatformService.java index 184a1b493..278262bb8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestReadPlatformService.java @@ -67,5 +67,5 @@ public interface LoanRescheduleRequestReadPlatformService { * pending for approval reject - give all rejected requests * @return list of LoanRescheduleRequestData objects **/ - List retrieveAllRescheduleRequests(String command); + List retrieveAllRescheduleRequests(String command, Long loanId); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestReadPlatformServiceImpl.java index c9fb65db4..ce6c3e8b3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestReadPlatformServiceImpl.java @@ -18,6 +18,8 @@ */ package org.apache.fineract.portfolio.loanaccount.rescheduleloan.service; +import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; + import java.math.BigDecimal; import java.sql.ResultSet; import java.sql.SQLException; @@ -271,19 +273,32 @@ public class LoanRescheduleRequestReadPlatformServiceImpl implements LoanResched } @Override - public List retrieveAllRescheduleRequests(String command) { + public List retrieveAllRescheduleRequests(String command, Long loanId) { LoanRescheduleRequestRowMapperForBulkApproval loanRescheduleRequestRowMapperForBulkApproval = new LoanRescheduleRequestRowMapperForBulkApproval(); String sql = "select " + loanRescheduleRequestRowMapperForBulkApproval.schema(); + List extraFilters = new ArrayList<>(); + List extraParams = new ArrayList<>(); + if (!StringUtils.isEmpty(command) && !command.equalsIgnoreCase(RescheduleLoansApiConstants.allCommandParamName)) { - sql = sql + " where lrr.status_enum = ? "; - Integer statusParam = 100; + int statusParam = 100; if (command.equalsIgnoreCase(RescheduleLoansApiConstants.approveCommandParamName)) { statusParam = 200; } else if (command.equalsIgnoreCase(RescheduleLoansApiConstants.rejectCommandParamName)) { statusParam = 300; } - return this.jdbcTemplate.query(sql, loanRescheduleRequestRowMapperForBulkApproval, statusParam); // NOSONAR + + extraFilters.add("lrr.status_enum = ?"); + extraParams.add(statusParam); } - return this.jdbcTemplate.query(sql, loanRescheduleRequestRowMapperForBulkApproval); // NOSONAR + + if (loanId != null) { + extraFilters.add("loan.id = ?"); + extraParams.add(loanId); + } + + if (isNotEmpty(extraFilters)) { + sql += " where " + String.join(" AND ", extraFilters); + } + return jdbcTemplate.query(sql, loanRescheduleRequestRowMapperForBulkApproval, extraParams.toArray()); // NOSONAR } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/service/SearchReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/service/SearchReadPlatformServiceImpl.java index 4bb98aaa7..a15e02fae 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/service/SearchReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/service/SearchReadPlatformServiceImpl.java @@ -23,9 +23,11 @@ import java.math.RoundingMode; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Collection; +import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.domain.JdbcSupport; +import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.organisation.office.data.OfficeData; import org.apache.fineract.organisation.office.service.OfficeReadPlatformService; @@ -42,33 +44,24 @@ import org.apache.fineract.portfolio.search.data.AdHocSearchQueryData; import org.apache.fineract.portfolio.search.data.SearchConditions; import org.apache.fineract.portfolio.search.data.SearchData; import org.apache.fineract.useradministration.domain.AppUser; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Service; @Service +@RequiredArgsConstructor public class SearchReadPlatformServiceImpl implements SearchReadPlatformService { private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; private final PlatformSecurityContext context; private final LoanProductReadPlatformService loanProductReadPlatformService; private final OfficeReadPlatformService officeReadPlatformService; - - @Autowired - public SearchReadPlatformServiceImpl(final PlatformSecurityContext context, final NamedParameterJdbcTemplate namedParameterJdbcTemplate, - final LoanProductReadPlatformService loanProductReadPlatformService, - final OfficeReadPlatformService officeReadPlatformService) { - this.context = context; - this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; - this.loanProductReadPlatformService = loanProductReadPlatformService; - this.officeReadPlatformService = officeReadPlatformService; - } + private final DatabaseSpecificSQLGenerator sqlGenerator; @Override public Collection retriveMatchingData(final SearchConditions searchConditions) { - final AppUser currentUser = this.context.authenticatedUser(); + final AppUser currentUser = context.authenticatedUser(); final String hierarchy = currentUser.getOffice().getHierarchy(); final SearchMapper rm = new SearchMapper(); @@ -80,73 +73,75 @@ public class SearchReadPlatformServiceImpl implements SearchReadPlatformService } else { params.addValue("search", "%" + searchConditions.getSearchQuery() + "%"); } - return this.namedParameterJdbcTemplate.query(rm.searchSchema(searchConditions), params, rm); + return namedParameterJdbcTemplate.query(searchSchema(searchConditions), params, rm); + } + + public String searchSchema(final SearchConditions searchConditions) { + + final String union = " union "; + final String clientMatchSql = " (select 'CLIENT' as entityType, c.id as entityId, c.display_name as entityName, c.external_id as entityExternalId, c.account_no as entityAccountNo " + + " , c.office_id as parentId, o.name as parentName, c.mobile_no as entityMobileNo,c.status_enum as entityStatusEnum, null as subEntityType, null as parentType " + + " from m_client c join m_office o on o.id = c.office_id where o.hierarchy like :hierarchy and (c.account_no like :search or c.display_name like :search or c.external_id like :search or c.mobile_no like :search)) "; + + final String loanMatchSql = " (select 'LOAN' as entityType, l.id as entityId, pl.name as entityName, l.external_id as entityExternalId, l.account_no as entityAccountNo " + + " , coalesce(c.id,g.id) as parentId, coalesce(c.display_name,g.display_name) as parentName, null as entityMobileNo, l.loan_status_id as entityStatusEnum, null as subEntityType, CASE WHEN g.id is null THEN 'client' ELSE 'group' END as parentType " + + " from m_loan l left join m_client c on l.client_id = c.id left join m_group g ON l.group_id = g.id left join m_office o on o.id = c.office_id left join m_product_loan pl on pl.id=l.product_id where (o.hierarchy IS NULL OR o.hierarchy like :hierarchy) and (l.account_no like :search or l.external_id like :search)) "; + + final String savingMatchSql = " (select 'SAVING' as entityType, s.id as entityId, sp.name as entityName, s.external_id as entityExternalId, s.account_no as entityAccountNo " + + " , coalesce(c.id,g.id) as parentId, coalesce(c.display_name,g.display_name) as parentName, null as entityMobileNo, s.status_enum as entityStatusEnum, s.deposit_type_enum as subEntityType, CASE WHEN g.id is null THEN 'client' ELSE 'group' END as parentType " + + " from m_savings_account s left join m_client c on s.client_id = c.id left join m_group g ON s.group_id = g.id left join m_office o on o.id = c.office_id left join m_savings_product sp on sp.id=s.product_id " + + " where (o.hierarchy IS NULL OR o.hierarchy like :hierarchy) and (s.account_no like :search or s.external_id like :search)) "; + + final String shareMatchSql = " (select 'SHARE' as entityType, s.id as entityId, sp.name as entityName, s.external_id as entityExternalId, s.account_no as entityAccountNo " + + " , c.id as parentId, c.display_name as parentName, null as entityMobileNo, s.status_enum as entityStatusEnum, null as subEntityType, 'client' as parentType " + + " from m_share_account s left join m_client c on s.client_id = c.id left join m_office o on o.id = c.office_id left join m_share_product sp on sp.id=s.product_id " + + " where (o.hierarchy IS NULL OR o.hierarchy like :hierarchy) and (s.account_no like :search or s.external_id like :search)) "; + + final String clientIdentifierMatchSql = " (select 'CLIENTIDENTIFIER' as entityType, ci.id as entityId, ci.document_key as entityName, " + + " null as entityExternalId, null as entityAccountNo, c.id as parentId, c.display_name as parentName,null as entityMobileNo, c.status_enum as entityStatusEnum, null as subEntityType, null as parentType " + + " from m_client_identifier ci join m_client c on ci.client_id=c.id join m_office o on o.id = c.office_id " + + " where o.hierarchy like :hierarchy and ci.document_key like :search ) "; + + final String groupMatchSql = " (select CASE WHEN g.level_id=1 THEN 'CENTER' ELSE 'GROUP' END as entityType, g.id as entityId, g.display_name as entityName, g.external_id as entityExternalId, g.account_no as entityAccountNo, " + + " g.office_id as parentId, o.name as parentName, null as entityMobileNo, g.status_enum as entityStatusEnum, null as subEntityType, null as parentType " + + " from m_group g join m_office o on o.id = g.office_id where o.hierarchy like :hierarchy and (g.account_no like :search or g.display_name like :search or g.external_id like :search or g.id like :search )) "; + + final StringBuilder sql = new StringBuilder(); + + if (searchConditions.isClientSearch()) { + sql.append(clientMatchSql).append(union); + } + + if (searchConditions.isLoanSeach()) { + sql.append(loanMatchSql).append(union); + } + + if (searchConditions.isSavingSeach()) { + sql.append(savingMatchSql).append(union); + } + + if (searchConditions.isShareSeach()) { + sql.append(shareMatchSql).append(union); + } + + if (searchConditions.isClientIdentifierSearch()) { + sql.append(clientIdentifierMatchSql).append(union); + } + + if (searchConditions.isGroupSearch()) { + sql.append(groupMatchSql).append(union); + } + + // remove last occurrence of "union all" string + sql.replace(sql.lastIndexOf(union), sql.length(), ""); + + // only get the first 50 rows in case of searcing + sql.append(" ").append(sqlGenerator.limit(50, 0)); + return sql.toString(); } private static final class SearchMapper implements RowMapper { - public String searchSchema(final SearchConditions searchConditions) { - - final String union = " union "; - final String clientMatchSql = " (select 'CLIENT' as entityType, c.id as entityId, c.display_name as entityName, c.external_id as entityExternalId, c.account_no as entityAccountNo " - + " , c.office_id as parentId, o.name as parentName, c.mobile_no as entityMobileNo,c.status_enum as entityStatusEnum, null as subEntityType, null as parentType " - + " from m_client c join m_office o on o.id = c.office_id where o.hierarchy like :hierarchy and (c.account_no like :search or c.display_name like :search or c.external_id like :search or c.mobile_no like :search)) "; - - final String loanMatchSql = " (select 'LOAN' as entityType, l.id as entityId, pl.name as entityName, l.external_id as entityExternalId, l.account_no as entityAccountNo " - + " , coalesce(c.id,g.id) as parentId, coalesce(c.display_name,g.display_name) as parentName, null as entityMobileNo, l.loan_status_id as entityStatusEnum, null as subEntityType, CASE WHEN g.id is null THEN 'client' ELSE 'group' END as parentType " - + " from m_loan l left join m_client c on l.client_id = c.id left join m_group g ON l.group_id = g.id left join m_office o on o.id = c.office_id left join m_product_loan pl on pl.id=l.product_id where (o.hierarchy IS NULL OR o.hierarchy like :hierarchy) and (l.account_no like :search or l.external_id like :search)) "; - - final String savingMatchSql = " (select 'SAVING' as entityType, s.id as entityId, sp.name as entityName, s.external_id as entityExternalId, s.account_no as entityAccountNo " - + " , coalesce(c.id,g.id) as parentId, coalesce(c.display_name,g.display_name) as parentName, null as entityMobileNo, s.status_enum as entityStatusEnum, s.deposit_type_enum as subEntityType, CASE WHEN g.id is null THEN 'client' ELSE 'group' END as parentType " - + " from m_savings_account s left join m_client c on s.client_id = c.id left join m_group g ON s.group_id = g.id left join m_office o on o.id = c.office_id left join m_savings_product sp on sp.id=s.product_id " - + " where (o.hierarchy IS NULL OR o.hierarchy like :hierarchy) and (s.account_no like :search or s.external_id like :search)) "; - - final String shareMatchSql = " (select 'SHARE' as entityType, s.id as entityId, sp.name as entityName, s.external_id as entityExternalId, s.account_no as entityAccountNo " - + " , c.id as parentId, c.display_name as parentName, null as entityMobileNo, s.status_enum as entityStatusEnum, null as subEntityType, 'client' as parentType " - + " from m_share_account s left join m_client c on s.client_id = c.id left join m_office o on o.id = c.office_id left join m_share_product sp on sp.id=s.product_id " - + " where (o.hierarchy IS NULL OR o.hierarchy like :hierarchy) and (s.account_no like :search or s.external_id like :search)) "; - - final String clientIdentifierMatchSql = " (select 'CLIENTIDENTIFIER' as entityType, ci.id as entityId, ci.document_key as entityName, " - + " null as entityExternalId, null as entityAccountNo, c.id as parentId, c.display_name as parentName,null as entityMobileNo, c.status_enum as entityStatusEnum, null as subEntityType, null as parentType " - + " from m_client_identifier ci join m_client c on ci.client_id=c.id join m_office o on o.id = c.office_id " - + " where o.hierarchy like :hierarchy and ci.document_key like :search ) "; - - final String groupMatchSql = " (select CASE WHEN g.level_id=1 THEN 'CENTER' ELSE 'GROUP' END as entityType, g.id as entityId, g.display_name as entityName, g.external_id as entityExternalId, g.account_no as entityAccountNo, " - + " g.office_id as parentId, o.name as parentName, null as entityMobileNo, g.status_enum as entityStatusEnum, null as subEntityType, null as parentType " - + " from m_group g join m_office o on o.id = g.office_id where o.hierarchy like :hierarchy and (g.account_no like :search or g.display_name like :search or g.external_id like :search or g.id like :search )) "; - - final StringBuilder sql = new StringBuilder(); - - if (searchConditions.isClientSearch()) { - sql.append(clientMatchSql).append(union); - } - - if (searchConditions.isLoanSeach()) { - sql.append(loanMatchSql).append(union); - } - - if (searchConditions.isSavingSeach()) { - sql.append(savingMatchSql).append(union); - } - - if (searchConditions.isShareSeach()) { - sql.append(shareMatchSql).append(union); - } - - if (searchConditions.isClientIdentifierSearch()) { - sql.append(clientIdentifierMatchSql).append(union); - } - - if (searchConditions.isGroupSearch()) { - sql.append(groupMatchSql).append(union); - } - - // remove last occurrence of "union all" string - sql.replace(sql.lastIndexOf(union), sql.length(), ""); - - return sql.toString(); - } - @Override public SearchData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { final Long entityId = JdbcSupport.getLong(rs, "entityId"); @@ -187,10 +182,10 @@ public class SearchReadPlatformServiceImpl implements SearchReadPlatformService @Override public AdHocSearchQueryData retrieveAdHocQueryTemplate() { - this.context.authenticatedUser(); + context.authenticatedUser(); - final Collection loanProducts = this.loanProductReadPlatformService.retrieveAllLoanProductsForLookup(); - final Collection offices = this.officeReadPlatformService.retrieveAllOfficesForDropdown(); + final Collection loanProducts = loanProductReadPlatformService.retrieveAllLoanProductsForLookup(); + final Collection offices = officeReadPlatformService.retrieveAllOfficesForDropdown(); return AdHocSearchQueryData.template(loanProducts, offices); } @@ -198,12 +193,12 @@ public class SearchReadPlatformServiceImpl implements SearchReadPlatformService @Override public Collection retrieveAdHocQueryMatchingData(final AdHocQuerySearchConditions searchConditions) { - this.context.authenticatedUser(); + context.authenticatedUser(); final AdHocQuerySearchMapper rm = new AdHocQuerySearchMapper(); final MapSqlParameterSource params = new MapSqlParameterSource(); - return this.namedParameterJdbcTemplate.query(rm.schema(searchConditions, params), params, rm); + return namedParameterJdbcTemplate.query(rm.schema(searchConditions, params), params, rm); } private static final class AdHocQuerySearchMapper implements RowMapper { @@ -272,15 +267,11 @@ public class SearchReadPlatformServiceImpl implements SearchReadPlatformService if (searchConditions.getIncludeOutStandingAmountPercentage()) { if (searchConditions.getOutStandingAmountPercentageCondition().equals("between")) { checkAndUpdateWhereClause(sql); - // params.addValue("outStandingAmountPercentageCondition", - // searchConditions.getOutStandingAmountPercentageCondition()); params.addValue("minOutStandingAmountPercentage", searchConditions.getMinOutStandingAmountPercentage()); params.addValue("maxOutStandingAmountPercentage", searchConditions.getMaxOutStandingAmountPercentage()); sql.append(" ( a.percentOut between :minOutStandingAmountPercentage and :maxOutStandingAmountPercentage ) "); } else { checkAndUpdateWhereClause(sql); - // params.addValue("outStandingAmountPercentageCondition", - // searchConditions.getOutStandingAmountPercentageCondition()); params.addValue("outStandingAmountPercentage", searchConditions.getOutStandingAmountPercentage()); sql.append(" a.percentOut ").append(searchConditions.getOutStandingAmountPercentageCondition()) .append(" :outStandingAmountPercentage "); @@ -290,15 +281,11 @@ public class SearchReadPlatformServiceImpl implements SearchReadPlatformService if (searchConditions.getIncludeOutstandingAmount()) { if (searchConditions.getOutstandingAmountCondition().equals("between")) { checkAndUpdateWhereClause(sql); - // params.addValue("outstandingAmountCondition", - // searchConditions.getOutstandingAmountCondition()); params.addValue("minOutstandingAmount", searchConditions.getMinOutstandingAmount()); params.addValue("maxOutstandingAmount", searchConditions.getMaxOutstandingAmount()); sql.append(" ( a.outstandingAmt between :minOutstandingAmount and :maxOutstandingAmount ) "); } else { checkAndUpdateWhereClause(sql); - // params.addValue("outstandingAmountCondition", - // searchConditions.getOutstandingAmountCondition()); params.addValue("outstandingAmount", searchConditions.getOutstandingAmount()); sql.append(" a.outstandingAmt ").append(searchConditions.getOutstandingAmountCondition()) .append(" :outstandingAmount ");