From 1d569d9ecf9cb11f1ab7220fc2b7fd1a4367f153 Mon Sep 17 00:00:00 2001 From: Oleksii Novikov Date: Wed, 19 Mar 2025 08:36:40 +0200 Subject: [PATCH] FINERACT-2181: Split Datatable service --- .../service/DatatableReadService.java | 55 ++ ...ervice.java => DatatableWriteService.java} | 34 +- .../DatatableEntryBusinessEvent.java | 0 .../DatatableEntryCreatedBusinessEvent.java | 0 .../DatatableEntryDeletedBusinessEvent.java | 0 .../datatable/DatatableEntryDetails.java | 0 .../DatatableEntryUpdatedBusinessEvent.java | 0 .../api/DatatablesApiResource.java | 25 +- .../CreateDatatableCommandHandler.java | 8 +- .../CreateDatatableEntryCommandHandler.java | 8 +- .../DeleteDatatableCommandHandler.java | 15 +- ...OneToManyDatatableEntryCommandHandler.java | 15 +- ...eOneToOneDatatableEntryCommandHandler.java | 15 +- .../RegisterDatatableCommandHandler.java | 20 +- .../UpdateDatatableCommandHandler.java | 8 +- ...OneToManyDatatableEntryCommandHandler.java | 16 +- ...eOneToOneDatatableEntryCommandHandler.java | 16 +- .../service/DatatableReadServiceImpl.java | 282 ++++++ .../dataqueries/service/DatatableUtil.java | 227 +++++ ...pl.java => DatatableWriteServiceImpl.java} | 880 +++++------------- ...atatableChecksReadPlatformServiceImpl.java | 27 +- ...tatableChecksWritePlatformServiceImpl.java | 11 +- .../starter/DataQueriesAutoConfiguration.java | 42 +- .../survey/service/ReadSurveyServiceImpl.java | 6 +- .../service/WriteSurveyServiceImpl.java | 14 +- ...sAccountTransactionsSearchServiceImpl.java | 6 +- .../savings/starter/SavingsConfiguration.java | 4 +- .../service/DatatableBusinessEventTest.java | 199 ++-- ...java => DatatableReadServiceImplTest.java} | 4 +- 29 files changed, 1045 insertions(+), 892 deletions(-) create mode 100644 fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableReadService.java rename fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/{ReadWriteNonCoreDataService.java => DatatableWriteService.java} (61%) rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryBusinessEvent.java (100%) rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryCreatedBusinessEvent.java (100%) rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryDeletedBusinessEvent.java (100%) rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryDetails.java (100%) rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryUpdatedBusinessEvent.java (100%) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableReadServiceImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableUtil.java rename fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/{ReadWriteNonCoreDataServiceImpl.java => DatatableWriteServiceImpl.java} (76%) rename fineract-provider/src/test/java/org/apache/fineract/infrastructure/dataqueries/service/{ReadWriteNonCoreDataServiceImplTest.java => DatatableReadServiceImplTest.java} (98%) diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableReadService.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableReadService.java new file mode 100644 index 000000000..f8c3d3e6b --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableReadService.java @@ -0,0 +1,55 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.dataqueries.service; + +import com.google.gson.JsonObject; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import java.util.Locale; +import org.apache.fineract.infrastructure.core.service.PagedLocalRequest; +import org.apache.fineract.infrastructure.dataqueries.data.DatatableData; +import org.apache.fineract.infrastructure.dataqueries.data.EntityTables; +import org.apache.fineract.infrastructure.dataqueries.data.GenericResultsetData; +import org.apache.fineract.portfolio.search.data.AdvancedQueryData; +import org.springframework.data.domain.Page; + +public interface DatatableReadService { + + List retrieveDatatableNames(String appTable); + + DatatableData retrieveDatatable(String datatable); + + List queryDataTable(@NotNull String datatable, @NotNull String columnName, String columnValue, + @NotNull String resultColumns); + + Page queryDataTableAdvanced(@NotNull String datatable, @NotNull PagedLocalRequest pagedRequest); + + boolean buildDataQueryEmbedded(@NotNull EntityTables entityTable, @NotNull String datatable, @NotNull AdvancedQueryData request, + @NotNull List selectColumns, @NotNull StringBuilder select, @NotNull StringBuilder from, @NotNull StringBuilder where, + @NotNull List params, String mainAlias, String alias, String dateFormat, String dateTimeFormat, Locale locale); + + GenericResultsetData retrieveDataTableGenericResultSet(String datatable, Long appTableId, String order, Long id); + + Long countDatatableEntries(String datatableName, Long appTableId, String foreignKeyColumn); + + String getTableName(String Url); + + String getDataTableName(String Url); + +} diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableWriteService.java similarity index 61% rename from fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableWriteService.java index 685a9f013..afa80ed28 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableWriteService.java @@ -18,25 +18,11 @@ */ package org.apache.fineract.infrastructure.dataqueries.service; -import com.google.gson.JsonObject; -import jakarta.validation.constraints.NotNull; -import java.util.List; -import java.util.Locale; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.infrastructure.core.service.PagedLocalRequest; -import org.apache.fineract.infrastructure.dataqueries.data.DatatableData; -import org.apache.fineract.infrastructure.dataqueries.data.EntityTables; -import org.apache.fineract.infrastructure.dataqueries.data.GenericResultsetData; -import org.apache.fineract.portfolio.search.data.AdvancedQueryData; -import org.springframework.data.domain.Page; import org.springframework.security.access.prepost.PreAuthorize; -public interface ReadWriteNonCoreDataService { - - List retrieveDatatableNames(String appTable); - - DatatableData retrieveDatatable(String datatable); +public interface DatatableWriteService { @PreAuthorize(value = "hasAnyAuthority('ALL_FUNCTIONS', 'REGISTER_DATATABLE')") void registerDatatable(JsonCommand command); @@ -50,8 +36,6 @@ public interface ReadWriteNonCoreDataService { @PreAuthorize(value = "hasAnyAuthority('ALL_FUNCTIONS', 'DEREGISTER_DATATABLE')") void deregisterDatatable(String datatable); - GenericResultsetData retrieveDataTableGenericResultSet(String datatable, Long appTableId, String order, Long id); - CommandProcessingResult createDatatable(JsonCommand command); void updateDatatable(String datatableName, JsonCommand command); @@ -62,8 +46,6 @@ public interface ReadWriteNonCoreDataService { CommandProcessingResult createNewDatatableEntry(String datatable, Long appTableId, String json); - CommandProcessingResult createPPIEntry(String datatable, Long appTableId, JsonCommand command); - CommandProcessingResult updateDatatableEntryOneToOne(String datatable, Long appTableId, JsonCommand command); CommandProcessingResult updateDatatableEntryOneToMany(String datatable, Long appTableId, Long datatableId, JsonCommand command); @@ -72,18 +54,6 @@ public interface ReadWriteNonCoreDataService { CommandProcessingResult deleteDatatableEntry(String datatable, Long appTableId, Long datatableId, JsonCommand command); - String getTableName(String Url); + CommandProcessingResult createPPIEntry(String datatable, Long appTableId, JsonCommand command); - String getDataTableName(String Url); - - Long countDatatableEntries(String datatableName, Long appTableId, String foreignKeyColumn); - - List queryDataTable(@NotNull String datatable, @NotNull String columnName, String columnValue, - @NotNull String resultColumns); - - Page queryDataTableAdvanced(@NotNull String datatable, @NotNull PagedLocalRequest pagedRequest); - - boolean buildDataQueryEmbedded(@NotNull EntityTables entityTable, @NotNull String datatable, @NotNull AdvancedQueryData request, - @NotNull List selectColumns, @NotNull StringBuilder select, @NotNull StringBuilder from, @NotNull StringBuilder where, - @NotNull List params, String mainAlias, String alias, String dateFormat, String dateTimeFormat, Locale locale); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryBusinessEvent.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryBusinessEvent.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryBusinessEvent.java rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryBusinessEvent.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryCreatedBusinessEvent.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryCreatedBusinessEvent.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryCreatedBusinessEvent.java rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryCreatedBusinessEvent.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryDeletedBusinessEvent.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryDeletedBusinessEvent.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryDeletedBusinessEvent.java rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryDeletedBusinessEvent.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryDetails.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryDetails.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryDetails.java rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryDetails.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryUpdatedBusinessEvent.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryUpdatedBusinessEvent.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryUpdatedBusinessEvent.java rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/domain/datatable/DatatableEntryUpdatedBusinessEvent.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DatatablesApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DatatablesApiResource.java index d36e01ea2..f11347ccb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DatatablesApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DatatablesApiResource.java @@ -55,8 +55,9 @@ import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer import org.apache.fineract.infrastructure.core.service.PagedLocalRequest; import org.apache.fineract.infrastructure.dataqueries.data.DatatableData; import org.apache.fineract.infrastructure.dataqueries.data.GenericResultsetData; +import org.apache.fineract.infrastructure.dataqueries.service.DatatableReadService; +import org.apache.fineract.infrastructure.dataqueries.service.DatatableWriteService; import org.apache.fineract.infrastructure.dataqueries.service.GenericDataService; -import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.search.data.AdvancedQueryData; import org.springframework.data.domain.Page; @@ -72,7 +73,8 @@ public class DatatablesApiResource { private final PlatformSecurityContext context; private final GenericDataService genericDataService; - private final ReadWriteNonCoreDataService readWriteNonCoreDataService; + private final DatatableReadService datatableReadService; + private final DatatableWriteService datatableWriteService; private final ToApiJsonSerializer toApiJsonSerializer; private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; @@ -87,7 +89,7 @@ public class DatatablesApiResource { public String getDatatables(@QueryParam("apptable") @Parameter(description = "apptable") final String apptable, @Context final UriInfo uriInfo) { - final List result = this.readWriteNonCoreDataService.retrieveDatatableNames(apptable); + final List result = this.datatableReadService.retrieveDatatableNames(apptable); return this.toApiJsonSerializer.serialize(result); } @@ -184,7 +186,7 @@ public class DatatablesApiResource { @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = DatatablesApiResourceSwagger.PutDataTablesResponse.class))) }) public String deregisterDatatable(@PathParam("datatable") @Parameter(description = "datatable") final String datatable) { - this.readWriteNonCoreDataService.deregisterDatatable(datatable); + this.datatableWriteService.deregisterDatatable(datatable); final CommandProcessingResult result = new CommandProcessingResultBuilder().withResourceIdAsString(datatable).build(); return this.toApiJsonSerializer.serialize(result); } @@ -199,7 +201,7 @@ public class DatatablesApiResource { public String getDatatable(@PathParam("datatable") @Parameter(description = "datatable") final String datatable, @Context final UriInfo uriInfo) { - final DatatableData result = this.readWriteNonCoreDataService.retrieveDatatable(datatable); + final DatatableData result = this.datatableReadService.retrieveDatatable(datatable); return this.toApiJsonSerializer.serialize(result); } @@ -216,8 +218,7 @@ public class DatatablesApiResource { @Context final UriInfo uriInfo) { this.context.authenticatedUser().validateHasDatatableReadPermission(datatable); - final List result = this.readWriteNonCoreDataService.queryDataTable(datatable, columnFilter, valueFilter, - resultColumns); + final List result = this.datatableReadService.queryDataTable(datatable, columnFilter, valueFilter, resultColumns); return this.toApiJsonSerializer.serialize(result); } @@ -231,7 +232,7 @@ public class DatatablesApiResource { @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = List.class))) }) public String advancedQuery(@PathParam("datatable") @Parameter(description = "datatable") final String datatable, PagedLocalRequest queryRequest, @Context final UriInfo uriInfo) { - final Page result = this.readWriteNonCoreDataService.queryDataTableAdvanced(datatable, queryRequest); + final Page result = this.datatableReadService.queryDataTableAdvanced(datatable, queryRequest); return this.toApiJsonSerializer.serialize(result); } @@ -254,8 +255,8 @@ public class DatatablesApiResource { this.context.authenticatedUser().validateHasDatatableReadPermission(datatable); - final GenericResultsetData results = this.readWriteNonCoreDataService.retrieveDataTableGenericResultSet(datatable, apptableId, - order, null); + final GenericResultsetData results = this.datatableReadService.retrieveDataTableGenericResultSet(datatable, apptableId, order, + null); String json = ""; final boolean genericResultSet = ApiParameterHelper.genericResultSet(uriInfo.getQueryParameters()); @@ -280,8 +281,8 @@ public class DatatablesApiResource { this.context.authenticatedUser().validateHasDatatableReadPermission(datatable); - final GenericResultsetData results = this.readWriteNonCoreDataService.retrieveDataTableGenericResultSet(datatable, apptableId, - order, datatableId); + final GenericResultsetData results = this.datatableReadService.retrieveDataTableGenericResultSet(datatable, apptableId, order, + datatableId); String json = ""; if (genericResultSet) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/CreateDatatableCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/CreateDatatableCommandHandler.java index 972ac19cb..95c8fdcc4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/CreateDatatableCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/CreateDatatableCommandHandler.java @@ -22,7 +22,7 @@ import lombok.RequiredArgsConstructor; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; +import org.apache.fineract.infrastructure.dataqueries.service.DatatableWriteService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -30,12 +30,12 @@ import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor public class CreateDatatableCommandHandler implements NewCommandSourceHandler { - private final ReadWriteNonCoreDataService writePlatformService; + private final DatatableWriteService datatableWriteService; @Transactional @Override public CommandProcessingResult processCommand(final JsonCommand command) { - - return writePlatformService.createDatatable(command); + return datatableWriteService.createDatatable(command); } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/CreateDatatableEntryCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/CreateDatatableEntryCommandHandler.java index 40b0a3b13..bb7db6e03 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/CreateDatatableEntryCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/CreateDatatableEntryCommandHandler.java @@ -22,7 +22,7 @@ import lombok.RequiredArgsConstructor; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; +import org.apache.fineract.infrastructure.dataqueries.service.DatatableWriteService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -30,12 +30,12 @@ import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor public class CreateDatatableEntryCommandHandler implements NewCommandSourceHandler { - private final ReadWriteNonCoreDataService writePlatformService; + private final DatatableWriteService datatableWriteService; @Transactional @Override public CommandProcessingResult processCommand(final JsonCommand command) { - - return writePlatformService.createNewDatatableEntry(command.getEntityName(), command.entityId(), command); + return datatableWriteService.createNewDatatableEntry(command.getEntityName(), command.entityId(), command); } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/DeleteDatatableCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/DeleteDatatableCommandHandler.java index 176c0bf57..5629a266b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/DeleteDatatableCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/DeleteDatatableCommandHandler.java @@ -18,30 +18,27 @@ */ package org.apache.fineract.infrastructure.dataqueries.handler; +import lombok.RequiredArgsConstructor; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; -import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; -import org.springframework.beans.factory.annotation.Autowired; +import org.apache.fineract.infrastructure.dataqueries.service.DatatableWriteService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service +@RequiredArgsConstructor public class DeleteDatatableCommandHandler implements NewCommandSourceHandler { - private final ReadWriteNonCoreDataService writePlatformService; - - @Autowired - public DeleteDatatableCommandHandler(final ReadWriteNonCoreDataService writePlatformService) { - this.writePlatformService = writePlatformService; - } + private final DatatableWriteService datatableWriteService; @Transactional @Override public CommandProcessingResult processCommand(final JsonCommand command) { final String datatableName = command.getUrl().replaceAll("/datatables/", ""); - this.writePlatformService.deleteDatatable(datatableName); + datatableWriteService.deleteDatatable(datatableName); return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withResourceIdAsString(datatableName).build(); } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/DeleteOneToManyDatatableEntryCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/DeleteOneToManyDatatableEntryCommandHandler.java index 597ee301c..c4cc09359 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/DeleteOneToManyDatatableEntryCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/DeleteOneToManyDatatableEntryCommandHandler.java @@ -18,27 +18,24 @@ */ package org.apache.fineract.infrastructure.dataqueries.handler; +import lombok.RequiredArgsConstructor; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; -import org.springframework.beans.factory.annotation.Autowired; +import org.apache.fineract.infrastructure.dataqueries.service.DatatableWriteService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service +@RequiredArgsConstructor public class DeleteOneToManyDatatableEntryCommandHandler implements NewCommandSourceHandler { - private final ReadWriteNonCoreDataService writePlatformService; - - @Autowired - public DeleteOneToManyDatatableEntryCommandHandler(final ReadWriteNonCoreDataService writePlatformService) { - this.writePlatformService = writePlatformService; - } + private final DatatableWriteService datatableWriteService; @Transactional @Override public CommandProcessingResult processCommand(final JsonCommand command) { - return this.writePlatformService.deleteDatatableEntry(command.getEntityName(), command.entityId(), command.subentityId(), command); + return datatableWriteService.deleteDatatableEntry(command.getEntityName(), command.entityId(), command.subentityId(), command); } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/DeleteOneToOneDatatableEntryCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/DeleteOneToOneDatatableEntryCommandHandler.java index d8efc9583..1a06db77a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/DeleteOneToOneDatatableEntryCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/DeleteOneToOneDatatableEntryCommandHandler.java @@ -18,27 +18,24 @@ */ package org.apache.fineract.infrastructure.dataqueries.handler; +import lombok.RequiredArgsConstructor; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; -import org.springframework.beans.factory.annotation.Autowired; +import org.apache.fineract.infrastructure.dataqueries.service.DatatableWriteService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service +@RequiredArgsConstructor public class DeleteOneToOneDatatableEntryCommandHandler implements NewCommandSourceHandler { - private final ReadWriteNonCoreDataService writePlatformService; - - @Autowired - public DeleteOneToOneDatatableEntryCommandHandler(final ReadWriteNonCoreDataService writePlatformService) { - this.writePlatformService = writePlatformService; - } + private final DatatableWriteService datatableWriteService; @Transactional @Override public CommandProcessingResult processCommand(final JsonCommand command) { - return this.writePlatformService.deleteDatatableEntries(command.getEntityName(), command.entityId(), command); + return datatableWriteService.deleteDatatableEntries(command.getEntityName(), command.entityId(), command); } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/RegisterDatatableCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/RegisterDatatableCommandHandler.java index 0d351de0c..b3f6d054b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/RegisterDatatableCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/RegisterDatatableCommandHandler.java @@ -18,32 +18,28 @@ */ package org.apache.fineract.infrastructure.dataqueries.handler; +import lombok.RequiredArgsConstructor; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; -import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; -import org.springframework.beans.factory.annotation.Autowired; +import org.apache.fineract.infrastructure.dataqueries.service.DatatableReadService; +import org.apache.fineract.infrastructure.dataqueries.service.DatatableWriteService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service +@RequiredArgsConstructor public class RegisterDatatableCommandHandler implements NewCommandSourceHandler { - private final ReadWriteNonCoreDataService writePlatformService; - - @Autowired - public RegisterDatatableCommandHandler(final ReadWriteNonCoreDataService writePlatformService) { - this.writePlatformService = writePlatformService; - } + private final DatatableWriteService datatableWriteService; + private final DatatableReadService datatableReadService; @Transactional @Override public CommandProcessingResult processCommand(final JsonCommand command) { + datatableWriteService.registerDatatable(command); - this.writePlatformService.registerDatatable(command); - - return new CommandProcessingResultBuilder().withResourceIdAsString(this.writePlatformService.getDataTableName(command.getUrl())) - .build(); + return new CommandProcessingResultBuilder().withResourceIdAsString(datatableReadService.getDataTableName(command.getUrl())).build(); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java index 9138442c7..f1461be50 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java @@ -23,7 +23,7 @@ import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; -import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; +import org.apache.fineract.infrastructure.dataqueries.service.DatatableWriteService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -31,16 +31,16 @@ import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor public class UpdateDatatableCommandHandler implements NewCommandSourceHandler { - private final ReadWriteNonCoreDataService writePlatformService; + private final DatatableWriteService datatableWriteService; @Transactional @Override public CommandProcessingResult processCommand(final JsonCommand command) { - final String datatableName = command.getUrl().replaceAll("/datatables/", ""); - this.writePlatformService.updateDatatable(datatableName, command); + datatableWriteService.updateDatatable(datatableName, command); return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withResourceIdAsString(datatableName).build(); } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/UpdateOneToManyDatatableEntryCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/UpdateOneToManyDatatableEntryCommandHandler.java index 29665e8f4..cc8ef6ccd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/UpdateOneToManyDatatableEntryCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/UpdateOneToManyDatatableEntryCommandHandler.java @@ -18,30 +18,26 @@ */ package org.apache.fineract.infrastructure.dataqueries.handler; +import lombok.RequiredArgsConstructor; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; -import org.springframework.beans.factory.annotation.Autowired; +import org.apache.fineract.infrastructure.dataqueries.service.DatatableWriteService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service +@RequiredArgsConstructor public class UpdateOneToManyDatatableEntryCommandHandler implements NewCommandSourceHandler { - private final ReadWriteNonCoreDataService writePlatformService; - - @Autowired - public UpdateOneToManyDatatableEntryCommandHandler(final ReadWriteNonCoreDataService writePlatformService) { - this.writePlatformService = writePlatformService; - } + private final DatatableWriteService datatableWriteService; @Transactional @Override public CommandProcessingResult processCommand(final JsonCommand command) { - - return this.writePlatformService.updateDatatableEntryOneToMany(command.getEntityName(), command.entityId(), command.subentityId(), + return this.datatableWriteService.updateDatatableEntryOneToMany(command.getEntityName(), command.entityId(), command.subentityId(), command); } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/UpdateOneToOneDatatableEntryCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/UpdateOneToOneDatatableEntryCommandHandler.java index f07b6ba10..45198a081 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/UpdateOneToOneDatatableEntryCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/UpdateOneToOneDatatableEntryCommandHandler.java @@ -18,30 +18,25 @@ */ package org.apache.fineract.infrastructure.dataqueries.handler; +import lombok.RequiredArgsConstructor; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; -import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; -import org.springframework.beans.factory.annotation.Autowired; +import org.apache.fineract.infrastructure.dataqueries.service.DatatableWriteService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service +@RequiredArgsConstructor public class UpdateOneToOneDatatableEntryCommandHandler implements NewCommandSourceHandler { - private final ReadWriteNonCoreDataService writePlatformService; - - @Autowired - public UpdateOneToOneDatatableEntryCommandHandler(final ReadWriteNonCoreDataService writePlatformService) { - this.writePlatformService = writePlatformService; - } + private final DatatableWriteService datatableWriteService; @Transactional @Override public CommandProcessingResult processCommand(final JsonCommand command) { - - final CommandProcessingResult commandProcessingResult = this.writePlatformService + final CommandProcessingResult commandProcessingResult = this.datatableWriteService .updateDatatableEntryOneToOne(command.getEntityName(), command.entityId(), command); return new CommandProcessingResultBuilder() // @@ -55,4 +50,5 @@ public class UpdateOneToOneDatatableEntryCommandHandler implements NewCommandSou .with(commandProcessingResult.getChanges()) // .build(); } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableReadServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableReadServiceImpl.java new file mode 100644 index 000000000..6e93d3c4c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableReadServiceImpl.java @@ -0,0 +1,282 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.dataqueries.service; + +import static java.util.Arrays.asList; +import static org.apache.fineract.infrastructure.core.service.database.SqlOperator.EQ; + +import com.google.common.base.Splitter; +import com.google.gson.JsonObject; +import jakarta.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.service.PagedLocalRequest; +import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; +import org.apache.fineract.infrastructure.dataqueries.data.DataTableValidator; +import org.apache.fineract.infrastructure.dataqueries.data.DatatableData; +import org.apache.fineract.infrastructure.dataqueries.data.EntityTables; +import org.apache.fineract.infrastructure.dataqueries.data.GenericResultsetData; +import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.infrastructure.security.service.SqlValidator; +import org.apache.fineract.portfolio.search.data.AdvancedQueryData; +import org.apache.fineract.portfolio.search.data.ColumnFilterData; +import org.apache.fineract.portfolio.search.service.SearchUtil; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.rowset.SqlRowSet; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +public class DatatableReadServiceImpl implements DatatableReadService { + + private static final String APPLICATION_TABLE_NAME = "application_table_name"; + + private final JdbcTemplate jdbcTemplate; + private final DatabaseSpecificSQLGenerator sqlGenerator; + private final PlatformSecurityContext context; + private final GenericDataService genericDataService; + private final DataTableValidator dataTableValidator; + private final SqlValidator sqlValidator; + private final SearchUtil searchUtil; + private final DatatableUtil datatableUtil; + + @Override + public List retrieveDatatableNames(final String appTable) { + // PERMITTED datatables + String sql = "select application_table_name, registered_table_name, entity_subtype from x_registered_table where exists" + + " (select 'f' from m_appuser_role ur join m_role r on r.id = ur.role_id" + + " left join m_role_permission rp on rp.role_id = r.id left join m_permission p on p.id = rp.permission_id" + + " where ur.appuser_id = ? and (p.code in ('ALL_FUNCTIONS', 'ALL_FUNCTIONS_READ') or p.code = concat" + + "('READ_', registered_table_name))) "; + + Object[] params; + if (appTable != null) { + sql = sql + " and application_table_name like ? "; + params = new Object[] { this.context.authenticatedUser().getId(), appTable }; + } else { + params = new Object[] { this.context.authenticatedUser().getId() }; + } + sql = sql + " order by application_table_name, registered_table_name"; + + final List datatables = new ArrayList<>(); + + final SqlRowSet rowSet = jdbcTemplate.queryForRowSet(sql, params); // NOSONAR + while (rowSet.next()) { + final String appTableName = rowSet.getString(APPLICATION_TABLE_NAME); + final String registeredDatatableName = rowSet.getString("registered_table_name"); + final String entitySubType = rowSet.getString("entity_subtype"); + final List columnHeaderData = genericDataService.fillResultsetColumnHeaders(registeredDatatableName); + + datatables.add(DatatableData.create(appTableName, registeredDatatableName, entitySubType, columnHeaderData)); + } + + return datatables; + } + + @Override + public DatatableData retrieveDatatable(final String datatable) { + // PERMITTED datatables + sqlValidator.validate(datatable); + final String sql = "select application_table_name, registered_table_name, entity_subtype from x_registered_table " + + " where exists (select 'f' from m_appuser_role ur join m_role r on r.id = ur.role_id" + + " left join m_role_permission rp on rp.role_id = r.id left join m_permission p on p.id = rp.permission_id" + + " where ur.appuser_id = ? and registered_table_name=? and (p.code in ('ALL_FUNCTIONS', " + + "'ALL_FUNCTIONS_READ') or p.code = concat('READ_', registered_table_name))) " + + " order by application_table_name, registered_table_name"; + + DatatableData datatableData = null; + + final SqlRowSet rowSet = jdbcTemplate.queryForRowSet(sql, new Object[] { this.context.authenticatedUser().getId(), datatable }); // NOSONAR + if (rowSet.next()) { + final String appTableName = rowSet.getString(APPLICATION_TABLE_NAME); + final String registeredDatatableName = rowSet.getString("registered_table_name"); + final String entitySubType = rowSet.getString("entity_subtype"); + final List columnHeaderData = this.genericDataService + .fillResultsetColumnHeaders(registeredDatatableName); + + datatableData = DatatableData.create(appTableName, registeredDatatableName, entitySubType, columnHeaderData); + } + + return datatableData; + } + + @Override + public List queryDataTable(@NotNull String datatable, @NotNull String columnName, String columnValueString, + @NotNull String resultColumnsString) { + datatable = datatableUtil.validateDatatableRegistered(datatable); + Map headersByName = searchUtil + .mapHeadersToName(genericDataService.fillResultsetColumnHeaders(datatable)); + + List resultColumns = asList(resultColumnsString.split(",")); + List selectColumns = searchUtil.validateToJdbcColumnNames(resultColumns, headersByName, false); + ResultsetColumnHeaderData column = searchUtil.validateToJdbcColumn(columnName, headersByName, false); + + Object columnValue = searchUtil.parseJdbcColumnValue(column, columnValueString, null, null, null, false, sqlGenerator); + String sql = sqlGenerator.buildSelect(selectColumns, null, false) + " " + sqlGenerator.buildFrom(datatable, null, false) + " WHERE " + + EQ.formatPlaceholder(sqlGenerator, column.getColumnName(), 1, null); + SqlRowSet rowSet = jdbcTemplate.queryForRowSet(sql, columnValue); // NOSONAR + + List results = new ArrayList<>(); + while (rowSet.next()) { + searchUtil.extractJsonResult(rowSet, selectColumns, resultColumns, results); + } + return results; + } + + @Override + public Page queryDataTableAdvanced(@NotNull String datatable, @NotNull PagedLocalRequest pagedRequest) { + datatable = datatableUtil.validateDatatableRegistered(datatable); + context.authenticatedUser().validateHasDatatableReadPermission(datatable); + + AdvancedQueryData request = pagedRequest.getRequest().orElseThrow(); + dataTableValidator.validateTableSearch(request); + + Map headersByName = searchUtil + .mapHeadersToName(genericDataService.fillResultsetColumnHeaders(datatable)); + String pkColumn = searchUtil.getFiltered(headersByName.values(), ResultsetColumnHeaderData::getIsColumnPrimaryKey).getColumnName(); + + List columnFilters = request.getNonNullFilters(); + columnFilters.forEach(e -> e.setColumn(searchUtil.validateToJdbcColumnName(e.getColumn(), headersByName, false))); + + List resultColumns = request.getNonNullResultColumns(); + List selectColumns; + if (resultColumns.isEmpty()) { + resultColumns.add(pkColumn); + selectColumns = new ArrayList<>(); + selectColumns.add(pkColumn); + } else { + selectColumns = searchUtil.validateToJdbcColumnNames(resultColumns, headersByName, false); + } + PageRequest pageable = pagedRequest.toPageable(); + PageRequest sortPageable; + if (pageable.getSort().isSorted()) { + List orders = pageable.getSort().toList(); + sortPageable = pageable.withSort(Sort.by(orders.stream() + .map(e -> e.withProperty(searchUtil.validateToJdbcColumnName(e.getProperty(), headersByName, false))).toList())); + } else { + pageable = pageable.withSort(Sort.Direction.DESC, pkColumn); + sortPageable = pageable; + } + + String dateFormat = pagedRequest.getDateFormat(); + String dateTimeFormat = pagedRequest.getDateTimeFormat(); + Locale locale = pagedRequest.getLocaleObject(); + + String select = sqlGenerator.buildSelect(selectColumns, null, false); + String from = " " + sqlGenerator.buildFrom(datatable, null, false); + StringBuilder where = new StringBuilder(); + ArrayList params = new ArrayList<>(); + searchUtil.buildQueryCondition(columnFilters, where, params, null, headersByName, dateFormat, dateTimeFormat, locale, false, + sqlGenerator); + + List results = new ArrayList<>(); + Object[] args = params.toArray(); + + // Execute the count Query + String countQuery = "SELECT COUNT(*)" + from + where; + Integer totalElements = jdbcTemplate.queryForObject(countQuery, Integer.class, args); // NOSONAR + if (totalElements == null || totalElements == 0) { + return PageableExecutionUtils.getPage(results, pageable, () -> 0); + } + + StringBuilder query = new StringBuilder().append(select).append(from).append(where); + query.append(" ").append(sqlGenerator.buildOrderBy(sortPageable.getSort().toList(), null, false)); + if (pageable.isPaged()) { + query.append(" ").append(sqlGenerator.limit(pageable.getPageSize(), (int) pageable.getOffset())); + } + + SqlRowSet rowSet = jdbcTemplate.queryForRowSet(query.toString(), args); + + while (rowSet.next()) { + searchUtil.extractJsonResult(rowSet, selectColumns, resultColumns, results); + } + return PageableExecutionUtils.getPage(results, pageable, () -> totalElements); + } + + @Override + public boolean buildDataQueryEmbedded(@NotNull EntityTables entityTable, @NotNull String datatable, @NotNull AdvancedQueryData request, + @NotNull List selectColumns, @NotNull StringBuilder select, @NotNull StringBuilder from, @NotNull StringBuilder where, + @NotNull List params, String mainAlias, String alias, String dateFormat, String dateTimeFormat, Locale locale) { + List resultColumns = request.getResultColumns(); + List columnFilters = request.getColumnFilters(); + if ((resultColumns == null || resultColumns.isEmpty()) && (columnFilters == null || columnFilters.isEmpty())) { + return false; + } + datatable = datatableUtil.validateDatatableRegistered(datatable); + context.authenticatedUser().validateHasDatatableReadPermission(datatable); + + Map headersByName = searchUtil + .mapHeadersToName(genericDataService.fillResultsetColumnHeaders(datatable)); + + List thisSelectColumns = searchUtil.validateToJdbcColumnNames(resultColumns, headersByName, true); + if (columnFilters != null) { + columnFilters.forEach(e -> e.setColumn(searchUtil.validateToJdbcColumnName(e.getColumn(), headersByName, false))); + } + + select.append(sqlGenerator.buildSelect(thisSelectColumns, alias, true)); + selectColumns.addAll(thisSelectColumns); + + String joinType = "LEFT"; + if (searchUtil.buildQueryCondition(columnFilters, where, params, alias, headersByName, dateFormat, dateTimeFormat, locale, true, + sqlGenerator)) { + joinType = null; // INNER + } + from.append(sqlGenerator.buildJoin(datatable, alias, entityTable.getForeignKeyColumnNameOnDatatable(), mainAlias, + entityTable.getRefColumn(), joinType)); + return true; + } + + @Override + @Transactional(readOnly = true) + public GenericResultsetData retrieveDataTableGenericResultSet(final String dataTableName, final Long appTableId, final String order, + final Long id) { + final EntityTables entityTable = datatableUtil.queryForApplicationEntity(dataTableName); + datatableUtil.checkMainResourceExistsWithinScope(entityTable, appTableId); + return datatableUtil.retrieveDataTableGenericResultSet(entityTable, dataTableName, appTableId, order, id); + } + + @Override + public Long countDatatableEntries(final String datatableName, final Long appTableId, String foreignKeyColumn) { + final String sqlString = "SELECT COUNT(" + sqlGenerator.escape(foreignKeyColumn) + ") FROM " + sqlGenerator.escape(datatableName) + + " WHERE " + sqlGenerator.escape(foreignKeyColumn) + " = " + appTableId; + return this.jdbcTemplate.queryForObject(sqlString, Long.class); // NOSONAR + } + + @Override + public String getDataTableName(String url) { + List urlParts = Splitter.on('/').splitToList(url); + return urlParts.get(3); + } + + @Override + public String getTableName(String url) { + List urlParts = Splitter.on('/').splitToList(url); + return urlParts.get(4); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableUtil.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableUtil.java new file mode 100644 index 000000000..2cbfaa7b1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableUtil.java @@ -0,0 +1,227 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.dataqueries.service; + +import static org.apache.fineract.infrastructure.core.serialization.DatatableCommandFromApiJsonDeserializer.DATATABLE_NAME_REGEX_PATTERN; +import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_FIELD_NAME; +import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.TABLE_FIELD_ID; +import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.TABLE_REGISTERED_TABLE; + +import jakarta.validation.constraints.NotNull; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; +import org.apache.fineract.infrastructure.dataqueries.data.EntityTables; +import org.apache.fineract.infrastructure.dataqueries.data.GenericResultsetData; +import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData; +import org.apache.fineract.infrastructure.dataqueries.data.ResultsetRowData; +import org.apache.fineract.infrastructure.dataqueries.exception.DatatableNotFoundException; +import org.apache.fineract.infrastructure.dataqueries.exception.DatatableSystemErrorException; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.infrastructure.security.service.SqlValidator; +import org.apache.fineract.infrastructure.security.utils.ColumnValidator; +import org.apache.fineract.portfolio.search.service.SearchUtil; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.rowset.SqlRowSet; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class DatatableUtil { + + private static final String APPLICATION_TABLE_NAME = "application_table_name"; + + private final SearchUtil searchUtil; + private final JdbcTemplate jdbcTemplate; + private final SqlValidator sqlValidator; + private final PlatformSecurityContext context; + private final GenericDataService genericDataService; + private final DatabaseSpecificSQLGenerator sqlGenerator; + private final ColumnValidator columnValidator; + + public boolean isMultirowDatatable(final List columnHeaders) { + return searchUtil.findFiltered(columnHeaders, e -> e.isNamed(TABLE_FIELD_ID)) != null; + } + + public String getFKField(EntityTables entityTable) { + return entityTable.getForeignKeyColumnNameOnDatatable(); + } + + public String validateDatatableRegistered(String datatable) { + validateDatatableName(datatable); + if (!isRegisteredDataTable(datatable)) { + throw new DatatableNotFoundException(datatable); + } + return datatable; + } + + public void validateDatatableName(final String name) { + if (name == null || name.isEmpty()) { + throw new PlatformDataIntegrityException("error.msg.datatables.datatable.null.name", "Data table name must not be blank."); + } else if (!name.matches(DATATABLE_NAME_REGEX_PATTERN)) { + throw new PlatformDataIntegrityException("error.msg.datatables.datatable.invalid.name.regex", "Invalid data table name.", name); + } + sqlValidator.validate(name); + } + + public EntityTables resolveEntity(final String entityName) { + EntityTables entityTable = EntityTables.fromEntityName(entityName); + if (entityTable == null) { + throw new PlatformDataIntegrityException("error.msg.invalid.application.table", "Invalid Datatable entity: " + entityName, + API_FIELD_NAME, entityName); + } + return entityTable; + } + + @NotNull + public EntityTables queryForApplicationEntity(final String datatable) { + sqlValidator.validate(datatable); + final String sql = "SELECT application_table_name FROM x_registered_table where registered_table_name = ?"; + final SqlRowSet rowSet = jdbcTemplate.queryForRowSet(sql, datatable); // NOSONAR + + String applicationTableName; + if (rowSet.next()) { + applicationTableName = rowSet.getString(APPLICATION_TABLE_NAME); + } else { + throw new DatatableNotFoundException(datatable); + } + return resolveEntity(applicationTableName); + } + + public CommandProcessingResult checkMainResourceExistsWithinScope(@NotNull EntityTables entityTable, final Long appTableId) { + final String sql = dataScopedSQL(entityTable, appTableId); + log.debug("data scoped sql: {}", sql); + final SqlRowSet rs = this.jdbcTemplate.queryForRowSet(sql); + + if (!rs.next()) { + throw new DatatableNotFoundException(entityTable, appTableId); + } + final Long officeId = (Long) rs.getObject("officeId"); + final Long groupId = (Long) rs.getObject("groupId"); + final Long clientId = (Long) rs.getObject("clientId"); + final Long savingsId = (Long) rs.getObject("savingsId"); + final Long loanId = (Long) rs.getObject("loanId"); + final Long transactionId = (Long) rs.getObject("transactionId"); + final Long entityId = (Long) rs.getObject("entityId"); + + if (rs.next()) { + throw new DatatableSystemErrorException("System Error: More than one row returned from data scoping query"); + } + + return new CommandProcessingResultBuilder() // + .withOfficeId(officeId) // + .withGroupId(groupId) // + .withClientId(clientId) // + .withSavingsId(savingsId) // + .withLoanId(loanId).withTransactionId(transactionId == null ? null : String.valueOf(transactionId)).withEntityId(entityId)// + .build(); + } + + public String dataScopedSQL(@NotNull EntityTables entityTable, final Long appTableId) { + // unfortunately have to, one way or another, be able to restrict data to the users office hierarchy. Here, a + // few key tables are done. But if additional fields are needed on other tables the same pattern applies + final AppUser currentUser = this.context.authenticatedUser(); + String officeHierarchy = currentUser.getOffice().getHierarchy(); + // m_loan and m_savings_account are connected to an m_office through either an m_client or an m_group If both it + // means it relates to an m_client that is in a group (still an m_client account) + return switch (entityTable) { + case LOAN -> "select distinct x.* from ( " + + "(select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId, null as transactionId, null as entityId from m_loan l " + + getClientOfficeJoinCondition(officeHierarchy, "l") + " where l.id = " + appTableId + ")" + " union all " + + "(select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId, null as transactionId, null as entityId from m_loan l " + + getGroupOfficeJoinCondition(officeHierarchy, "l") + " where l.id = " + appTableId + ")" + " ) as x"; + case SAVINGS -> "select distinct x.* from ( " + + "(select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, null as transactionId, null as entityId " + + "from m_savings_account s " + getClientOfficeJoinCondition(officeHierarchy, "s") + " where s.id = " + appTableId + ")" + + " union all " + + "(select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, null as transactionId, null as entityId " + + "from m_savings_account s " + getGroupOfficeJoinCondition(officeHierarchy, "s") + " where s.id = " + appTableId + ")" + + " ) as x"; + case SAVINGS_TRANSACTION -> "select distinct x.* from ( " + + "(select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, t.id as transactionId, null as entityId " + + "from m_savings_account_transaction t join m_savings_account s on t.savings_account_id = s.id " + + getClientOfficeJoinCondition(officeHierarchy, "s") + " where t.id = " + appTableId + ")" + " union all " + + "(select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, t.id as transactionId, null as entityId " + + "from m_savings_account_transaction t join m_savings_account s on t.savings_account_id = s.id " + + getGroupOfficeJoinCondition(officeHierarchy, "s") + " where t.id = " + appTableId + ")" + " ) as x"; + case CLIENT -> + "select o.id as officeId, null as groupId, c.id as clientId, null as savingsId, null as loanId, null as transactionId, null as entityId from m_client c " + + getOfficeJoinCondition(officeHierarchy, "c") + " where c.id = " + appTableId; + case GROUP, CENTER -> + "select o.id as officeId, g.id as groupId, null as clientId, null as savingsId, null as loanId, null as transactionId, null as entityId from m_group g " + + getOfficeJoinCondition(officeHierarchy, "g") + " where g.id = " + appTableId; + case OFFICE -> + "select o.id as officeId, null as groupId, null as clientId, null as savingsId, null as loanId, null as transactionId, null as entityId from m_office o " + + "where o.hierarchy like '" + officeHierarchy + "%'" + " and o.id = " + appTableId; + case LOAN_PRODUCT, SAVINGS_PRODUCT, SHARE_PRODUCT -> + "select null as officeId, null as groupId, null as clientId, null as savingsId, null as loanId, null as transactionId, p.id as entityId from " + + entityTable.getName() + " as p WHERE p.id = " + appTableId; + default -> throw new PlatformDataIntegrityException("error.msg.invalid.dataScopeCriteria", + "Application Table: " + entityTable.getName() + " not catered for in data Scoping"); + }; + } + + public String getOfficeJoinCondition(String officeHierarchy, String joinTableAlias) { + return " join m_office o on o.id = " + joinTableAlias + ".office_id and o.hierarchy like '" + officeHierarchy + "%' "; + } + + public String getGroupOfficeJoinCondition(String officeHierarchy, String appTableAlias) { + return " join m_group g on g.id = " + appTableAlias + ".group_id " + getOfficeJoinCondition(officeHierarchy, "g"); + } + + public String getClientOfficeJoinCondition(String officeHierarchy, String appTableAlias) { + return " join m_client c on c.id = " + appTableAlias + ".client_id " + getOfficeJoinCondition(officeHierarchy, "c"); + } + + public GenericResultsetData retrieveDataTableGenericResultSet(final EntityTables entityTable, final String dataTableName, + final Long appTableId, final String order, final Long id) { + final List columnHeaders = genericDataService.fillResultsetColumnHeaders(dataTableName); + final boolean multiRow = isMultirowDatatable(columnHeaders); + + String whereClause = getFKField(entityTable) + " = " + appTableId; + sqlValidator.validate(whereClause); + String sql = "select * from " + sqlGenerator.escape(dataTableName) + " where " + whereClause; + + // id only used for reading a specific entry that belongs to appTableId (in a one to many datatable) + if (multiRow && id != null) { + sql = sql + " and " + TABLE_FIELD_ID + " = " + id; + } + if (StringUtils.isNotBlank(order)) { + columnValidator.validateSqlInjection(sql, order); + sql = sql + " order by " + order; + } + + final List result = genericDataService.fillResultsetRowData(sql, columnHeaders); + return new GenericResultsetData(columnHeaders, result); + } + + private boolean isRegisteredDataTable(final String datatable) { + final String sql = "SELECT COUNT(application_table_name) FROM " + TABLE_REGISTERED_TABLE + " WHERE registered_table_name = ?"; + final Integer count = jdbcTemplate.queryForObject(sql, Integer.class, datatable); + return count != null && count > 0; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableWriteServiceImpl.java similarity index 76% rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java rename to fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableWriteServiceImpl.java index 91e9a47b1..9e2480197 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableWriteServiceImpl.java @@ -18,11 +18,8 @@ */ package org.apache.fineract.infrastructure.dataqueries.service; -import static java.util.Arrays.asList; -import static org.apache.fineract.infrastructure.core.serialization.DatatableCommandFromApiJsonDeserializer.DATATABLE_NAME_REGEX_PATTERN; import static org.apache.fineract.infrastructure.core.service.database.JdbcJavaType.BIGINT; import static org.apache.fineract.infrastructure.core.service.database.JdbcJavaType.DATETIME; -import static org.apache.fineract.infrastructure.core.service.database.SqlOperator.EQ; import static org.apache.fineract.infrastructure.core.service.database.SqlOperator.IN; import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_FIELD_AFTER; import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_FIELD_CODE; @@ -45,13 +42,11 @@ import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiCon import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_PARAM_SUBTYPE; import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.CREATEDAT_FIELD_NAME; import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.TABLE_FIELD_ID; -import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.TABLE_REGISTERED_TABLE; import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.UPDATEDAT_FIELD_NAME; import static org.apache.fineract.portfolio.search.SearchConstants.API_PARAM_DATETIME_FORMAT; import static org.apache.fineract.portfolio.search.SearchConstants.API_PARAM_DATE_FORMAT; import static org.apache.fineract.portfolio.search.SearchConstants.API_PARAM_LOCALE; -import com.google.common.base.Splitter; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -91,53 +86,39 @@ import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; -import org.apache.fineract.infrastructure.core.service.PagedLocalRequest; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; import org.apache.fineract.infrastructure.core.service.database.DatabaseType; import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver; import org.apache.fineract.infrastructure.core.service.database.JdbcJavaType; import org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant; import org.apache.fineract.infrastructure.dataqueries.data.DataTableValidator; -import org.apache.fineract.infrastructure.dataqueries.data.DatatableData; import org.apache.fineract.infrastructure.dataqueries.data.EntityTables; import org.apache.fineract.infrastructure.dataqueries.data.GenericResultsetData; import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData; -import org.apache.fineract.infrastructure.dataqueries.data.ResultsetRowData; import org.apache.fineract.infrastructure.dataqueries.exception.DatatableEntryRequiredException; import org.apache.fineract.infrastructure.dataqueries.exception.DatatableNotFoundException; -import org.apache.fineract.infrastructure.dataqueries.exception.DatatableSystemErrorException; import org.apache.fineract.infrastructure.event.business.domain.datatable.DatatableEntryCreatedBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.datatable.DatatableEntryDeletedBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.datatable.DatatableEntryDetails; import org.apache.fineract.infrastructure.event.business.domain.datatable.DatatableEntryUpdatedBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; -import org.apache.fineract.infrastructure.security.service.SqlValidator; -import org.apache.fineract.infrastructure.security.utils.ColumnValidator; -import org.apache.fineract.portfolio.search.data.AdvancedQueryData; -import org.apache.fineract.portfolio.search.data.ColumnFilterData; import org.apache.fineract.portfolio.search.service.SearchUtil; -import org.apache.fineract.useradministration.domain.AppUser; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.EmptyResultDataAccessException; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Sort; -import org.springframework.data.support.PageableExecutionUtils; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.rowset.SqlRowSet; import org.springframework.orm.jpa.JpaSystemException; import org.springframework.transaction.annotation.Transactional; @Slf4j @RequiredArgsConstructor -public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataService { +public class DatatableWriteServiceImpl implements DatatableWriteService { private static final String CODE_VALUES_TABLE = "m_code_value"; - private static final String APPLICATION_TABLE_NAME = "application_table_name"; + private static final String ENTITY_SUB_TYPE = "entity_subtype"; private static final String ERROR_MSG_DATATABLE_COLUMN_MISSING_UPDATE_PARSE = "error.msg.datatable.column.missing.update.parse"; private static final String DOES_NOT_EXIST = "does.not.exist"; @@ -158,198 +139,12 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ private final ConfigurationDomainService configurationDomainService; private final CodeReadPlatformService codeReadPlatformService; private final DataTableValidator dataTableValidator; - private final ColumnValidator columnValidator; private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; private final DatatableKeywordGenerator datatableKeywordGenerator; - private final SqlValidator sqlValidator; private final SearchUtil searchUtil; private final BusinessEventNotifierService businessEventNotifierService; - - @Override - public List retrieveDatatableNames(final String appTable) { - // PERMITTED datatables - String sql = "select application_table_name, registered_table_name, entity_subtype from x_registered_table where exists" - + " (select 'f' from m_appuser_role ur join m_role r on r.id = ur.role_id" - + " left join m_role_permission rp on rp.role_id = r.id left join m_permission p on p.id = rp.permission_id" - + " where ur.appuser_id = ? and (p.code in ('ALL_FUNCTIONS', 'ALL_FUNCTIONS_READ') or p.code = concat" - + "('READ_', registered_table_name))) "; - - Object[] params; - if (appTable != null) { - sql = sql + " and application_table_name like ? "; - params = new Object[] { this.context.authenticatedUser().getId(), appTable }; - } else { - params = new Object[] { this.context.authenticatedUser().getId() }; - } - sql = sql + " order by application_table_name, registered_table_name"; - - final List datatables = new ArrayList<>(); - - final SqlRowSet rowSet = jdbcTemplate.queryForRowSet(sql, params); // NOSONAR - while (rowSet.next()) { - final String appTableName = rowSet.getString(APPLICATION_TABLE_NAME); - final String registeredDatatableName = rowSet.getString("registered_table_name"); - final String entitySubType = rowSet.getString("entity_subtype"); - final List columnHeaderData = genericDataService.fillResultsetColumnHeaders(registeredDatatableName); - - datatables.add(DatatableData.create(appTableName, registeredDatatableName, entitySubType, columnHeaderData)); - } - - return datatables; - } - - @Override - public DatatableData retrieveDatatable(final String datatable) { - // PERMITTED datatables - sqlValidator.validate(datatable); - final String sql = "select application_table_name, registered_table_name, entity_subtype from x_registered_table " - + " where exists (select 'f' from m_appuser_role ur join m_role r on r.id = ur.role_id" - + " left join m_role_permission rp on rp.role_id = r.id left join m_permission p on p.id = rp.permission_id" - + " where ur.appuser_id = ? and registered_table_name=? and (p.code in ('ALL_FUNCTIONS', " - + "'ALL_FUNCTIONS_READ') or p.code = concat('READ_', registered_table_name))) " - + " order by application_table_name, registered_table_name"; - - DatatableData datatableData = null; - - final SqlRowSet rowSet = jdbcTemplate.queryForRowSet(sql, new Object[] { this.context.authenticatedUser().getId(), datatable }); // NOSONAR - if (rowSet.next()) { - final String appTableName = rowSet.getString(APPLICATION_TABLE_NAME); - final String registeredDatatableName = rowSet.getString("registered_table_name"); - final String entitySubType = rowSet.getString("entity_subtype"); - final List columnHeaderData = this.genericDataService - .fillResultsetColumnHeaders(registeredDatatableName); - - datatableData = DatatableData.create(appTableName, registeredDatatableName, entitySubType, columnHeaderData); - } - - return datatableData; - } - - @Override - public List queryDataTable(@NotNull String datatable, @NotNull String columnName, String columnValueString, - @NotNull String resultColumnsString) { - datatable = validateDatatableRegistered(datatable); - Map headersByName = searchUtil - .mapHeadersToName(genericDataService.fillResultsetColumnHeaders(datatable)); - - List resultColumns = asList(resultColumnsString.split(",")); - List selectColumns = searchUtil.validateToJdbcColumnNames(resultColumns, headersByName, false); - ResultsetColumnHeaderData column = searchUtil.validateToJdbcColumn(columnName, headersByName, false); - - Object columnValue = searchUtil.parseJdbcColumnValue(column, columnValueString, null, null, null, false, sqlGenerator); - String sql = sqlGenerator.buildSelect(selectColumns, null, false) + " " + sqlGenerator.buildFrom(datatable, null, false) + " WHERE " - + EQ.formatPlaceholder(sqlGenerator, column.getColumnName(), 1, null); - SqlRowSet rowSet = jdbcTemplate.queryForRowSet(sql, columnValue); // NOSONAR - - List results = new ArrayList<>(); - while (rowSet.next()) { - searchUtil.extractJsonResult(rowSet, selectColumns, resultColumns, results); - } - return results; - } - - @Override - public Page queryDataTableAdvanced(@NotNull String datatable, @NotNull PagedLocalRequest pagedRequest) { - datatable = validateDatatableRegistered(datatable); - context.authenticatedUser().validateHasDatatableReadPermission(datatable); - - AdvancedQueryData request = pagedRequest.getRequest().orElseThrow(); - dataTableValidator.validateTableSearch(request); - - Map headersByName = searchUtil - .mapHeadersToName(genericDataService.fillResultsetColumnHeaders(datatable)); - String pkColumn = searchUtil.getFiltered(headersByName.values(), ResultsetColumnHeaderData::getIsColumnPrimaryKey).getColumnName(); - - List columnFilters = request.getNonNullFilters(); - columnFilters.forEach(e -> e.setColumn(searchUtil.validateToJdbcColumnName(e.getColumn(), headersByName, false))); - - List resultColumns = request.getNonNullResultColumns(); - List selectColumns; - if (resultColumns.isEmpty()) { - resultColumns.add(pkColumn); - selectColumns = new ArrayList<>(); - selectColumns.add(pkColumn); - } else { - selectColumns = searchUtil.validateToJdbcColumnNames(resultColumns, headersByName, false); - } - PageRequest pageable = pagedRequest.toPageable(); - PageRequest sortPageable; - if (pageable.getSort().isSorted()) { - List orders = pageable.getSort().toList(); - sortPageable = pageable.withSort(Sort.by(orders.stream() - .map(e -> e.withProperty(searchUtil.validateToJdbcColumnName(e.getProperty(), headersByName, false))).toList())); - } else { - pageable = pageable.withSort(Sort.Direction.DESC, pkColumn); - sortPageable = pageable; - } - - String dateFormat = pagedRequest.getDateFormat(); - String dateTimeFormat = pagedRequest.getDateTimeFormat(); - Locale locale = pagedRequest.getLocaleObject(); - - String select = sqlGenerator.buildSelect(selectColumns, null, false); - String from = " " + sqlGenerator.buildFrom(datatable, null, false); - StringBuilder where = new StringBuilder(); - ArrayList params = new ArrayList<>(); - searchUtil.buildQueryCondition(columnFilters, where, params, null, headersByName, dateFormat, dateTimeFormat, locale, false, - sqlGenerator); - - List results = new ArrayList<>(); - Object[] args = params.toArray(); - - // Execute the count Query - String countQuery = "SELECT COUNT(*)" + from + where; - Integer totalElements = jdbcTemplate.queryForObject(countQuery, Integer.class, args); // NOSONAR - if (totalElements == null || totalElements == 0) { - return PageableExecutionUtils.getPage(results, pageable, () -> 0); - } - - StringBuilder query = new StringBuilder().append(select).append(from).append(where); - query.append(" ").append(sqlGenerator.buildOrderBy(sortPageable.getSort().toList(), null, false)); - if (pageable.isPaged()) { - query.append(" ").append(sqlGenerator.limit(pageable.getPageSize(), (int) pageable.getOffset())); - } - - SqlRowSet rowSet = jdbcTemplate.queryForRowSet(query.toString(), args); - - while (rowSet.next()) { - searchUtil.extractJsonResult(rowSet, selectColumns, resultColumns, results); - } - return PageableExecutionUtils.getPage(results, pageable, () -> totalElements); - } - - @Override - public boolean buildDataQueryEmbedded(@NotNull EntityTables entityTable, @NotNull String datatable, @NotNull AdvancedQueryData request, - @NotNull List selectColumns, @NotNull StringBuilder select, @NotNull StringBuilder from, @NotNull StringBuilder where, - @NotNull List params, String mainAlias, String alias, String dateFormat, String dateTimeFormat, Locale locale) { - List resultColumns = request.getResultColumns(); - List columnFilters = request.getColumnFilters(); - if ((resultColumns == null || resultColumns.isEmpty()) && (columnFilters == null || columnFilters.isEmpty())) { - return false; - } - datatable = validateDatatableRegistered(datatable); - context.authenticatedUser().validateHasDatatableReadPermission(datatable); - - Map headersByName = searchUtil - .mapHeadersToName(genericDataService.fillResultsetColumnHeaders(datatable)); - - List thisSelectColumns = searchUtil.validateToJdbcColumnNames(resultColumns, headersByName, true); - if (columnFilters != null) { - columnFilters.forEach(e -> e.setColumn(searchUtil.validateToJdbcColumnName(e.getColumn(), headersByName, false))); - } - - select.append(sqlGenerator.buildSelect(thisSelectColumns, alias, true)); - selectColumns.addAll(thisSelectColumns); - - String joinType = "LEFT"; - if (searchUtil.buildQueryCondition(columnFilters, where, params, alias, headersByName, dateFormat, dateTimeFormat, locale, true, - sqlGenerator)) { - joinType = null; // INNER - } - from.append(sqlGenerator.buildJoin(datatable, alias, entityTable.getForeignKeyColumnNameOnDatatable(), mainAlias, - entityTable.getRefColumn(), joinType)); - return true; - } + private final DatatableReadService datatableReadService; + private final DatatableUtil datatableUtil; @Transactional @Override @@ -363,8 +158,8 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ @Transactional @Override public void registerDatatable(final JsonCommand command) { - final String applicationTableName = this.getTableName(command.getUrl()); - final String dataTableName = this.getDataTableName(command.getUrl()); + final String applicationTableName = datatableReadService.getTableName(command.getUrl()); + final String dataTableName = datatableReadService.getDataTableName(command.getUrl()); final String entitySubType = command.stringValueOfParameterNamed(ENTITY_SUB_TYPE); Integer category = this.getCategory(command); @@ -377,8 +172,8 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ @Transactional @Override public void registerDatatable(final JsonCommand command, final String permissionSql) { - final String applicationTableName = this.getTableName(command.getUrl()); - final String dataTableName = this.getDataTableName(command.getUrl()); + final String applicationTableName = datatableReadService.getTableName(command.getUrl()); + final String dataTableName = datatableReadService.getDataTableName(command.getUrl()); final String entitySubType = command.stringValueOfParameterNamed(ENTITY_SUB_TYPE); Integer category = this.getCategory(command); @@ -388,80 +183,10 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ this.registerDataTable(applicationTableName, dataTableName, entitySubType, category, permissionSql); } - private void registerDataTable(final String entityName, final String dataTableName, final String entitySubType, final Integer category, - final String permissionsSql) { - resolveEntity(entityName); - validateDatatableName(dataTableName); - validateDataTableExists(dataTableName); - - Map paramMap = new HashMap<>(3); - final String registerDatatableSql = "insert into x_registered_table " - + "(registered_table_name, application_table_name, entity_subtype, category) " - + "values (:dataTableName, :applicationTableName, :entitySubType, :category)"; - paramMap.put("dataTableName", dataTableName); - paramMap.put("applicationTableName", entityName); - paramMap.put("entitySubType", entitySubType); - paramMap.put("category", category); - - try { - this.namedParameterJdbcTemplate.update(registerDatatableSql, paramMap); - this.jdbcTemplate.update(permissionsSql); - - // add the registered table to the config if it is a ppi - if (category.equals(DataTableApiConstant.CATEGORY_PPI)) { - this.namedParameterJdbcTemplate - .update("insert into c_configuration (name, value, enabled ) values( :dataTableName, '0', false)", paramMap); - } - - } catch (final JpaSystemException | DataIntegrityViolationException dve) { - handleDataIntegrityIssues(dataTableName, null, dve.getMostSpecificCause(), dve); - } catch (final PersistenceException dve) { - handleDataIntegrityIssues(dataTableName, null, ExceptionUtils.getRootCause(dve.getCause()), dve); - - } - } - - private String getPermissionSql(final String dataTableName) { - final String createPermission = "'CREATE_" + dataTableName + "'"; - final String createPermissionChecker = "'CREATE_" + dataTableName + "_CHECKER'"; - final String readPermission = "'READ_" + dataTableName + "'"; - final String updatePermission = "'UPDATE_" + dataTableName + "'"; - final String updatePermissionChecker = "'UPDATE_" + dataTableName + "_CHECKER'"; - final String deletePermission = "'DELETE_" + dataTableName + "'"; - final String deletePermissionChecker = "'DELETE_" + dataTableName + "_CHECKER'"; - final List escapedColumns = Stream.of("grouping", "code", "action_name", "entity_name", "can_maker_checker") - .map(sqlGenerator::escape).toList(); - final String columns = String.join(", ", escapedColumns); - - return "insert into m_permission (" + columns + ") values " + "('datatable', " + createPermission + ", 'CREATE', '" + dataTableName - + "', true)," + "('datatable', " + createPermissionChecker + ", 'CREATE', '" + dataTableName + "', false)," - + "('datatable', " + readPermission + ", 'READ', '" + dataTableName + "', false)," + "('datatable', " + updatePermission - + ", 'UPDATE', '" + dataTableName + "', true)," + "('datatable', " + updatePermissionChecker + ", 'UPDATE', '" - + dataTableName + "', false)," + "('datatable', " + deletePermission + ", 'DELETE', '" + dataTableName + "', true)," - + "('datatable', " + deletePermissionChecker + ", 'DELETE', '" + dataTableName + "', false)"; - } - - private Integer getCategory(final JsonCommand command) { - Integer category = command.integerValueOfParameterNamedDefaultToNullIfZero(DataTableApiConstant.categoryParamName); - return category == null ? DataTableApiConstant.CATEGORY_DEFAULT : category; - } - - @Override - public String getDataTableName(String url) { - List urlParts = Splitter.on('/').splitToList(url); - return urlParts.get(3); - } - - @Override - public String getTableName(String url) { - List urlParts = Splitter.on('/').splitToList(url); - return urlParts.get(4); - } - @Transactional @Override public void deregisterDatatable(final String datatable) { - validateDatatableRegistered(datatable); + datatableUtil.validateDatatableRegistered(datatable); final String permissionList = "('CREATE_" + datatable + "', 'CREATE_" + datatable + "_CHECKER', 'READ_" + datatable + "', 'UPDATE_" + datatable + "', 'UPDATE_" + datatable + "_CHECKER', 'DELETE_" + datatable + "', 'DELETE_" + datatable + "_CHECKER')"; @@ -481,47 +206,6 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ this.jdbcTemplate.batchUpdate(sqlArray); // NOSONAR } - private void parseDatatableColumnObjectForCreate(final JsonObject column, StringBuilder sqlBuilder, - final StringBuilder constrainBuilder, final String dataTableNameAlias, final Map codeMappings, - final boolean isConstraintApproach) { - String name = column.has(API_FIELD_NAME) ? column.get(API_FIELD_NAME).getAsString() : null; - final String type = column.has(API_FIELD_TYPE) ? column.get(API_FIELD_TYPE).getAsString().toLowerCase() : null; - final Integer length = column.has(API_FIELD_LENGTH) ? column.get(API_FIELD_LENGTH).getAsInt() : null; - final boolean mandatory = column.has(API_FIELD_MANDATORY) && column.get(API_FIELD_MANDATORY).getAsBoolean(); - final boolean unique = column.has(API_FIELD_UNIQUE) && column.get(API_FIELD_UNIQUE).getAsBoolean(); - final String code = column.has(API_FIELD_CODE) ? column.get(API_FIELD_CODE).getAsString() : null; - - if (StringUtils.isNotBlank(code)) { - if (isConstraintApproach) { - codeMappings.put(dataTableNameAlias + "_" + name, this.codeReadPlatformService.retriveCode(code).getId()); - String fkName = "fk_" + dataTableNameAlias + "_" + name; - constrainBuilder.append(", CONSTRAINT ").append(sqlGenerator.escape(fkName)).append(" ").append("FOREIGN KEY (") - .append(sqlGenerator.escape(name)).append(") ").append(REFERENCES_CLAUSE) - .append(sqlGenerator.escape(CODE_VALUES_TABLE)).append(" (id)"); - } else { - name = datatableColumnNameToCodeValueName(name, code); - } - } - sqlBuilder.append(sqlGenerator.escape(name)); - if (type != null) { - sqlBuilder.append(" ").append(mapApiTypeToDbType(type, length)); - } - - if (unique) { - String uniqueKeyName = datatableKeywordGenerator.generateUniqueKeyName(dataTableNameAlias, name); - constrainBuilder.append(", CONSTRAINT ").append(sqlGenerator.escape(uniqueKeyName)).append(" ").append("UNIQUE (") - .append(sqlGenerator.escape(name)).append(")"); - } - - if (mandatory) { - sqlBuilder.append(NOT_NULL_CLAUSE); - } else { - sqlBuilder.append(DEFAULT_NULL); - } - - sqlBuilder.append(", "); - } - @Transactional @Override public CommandProcessingResult createDatatable(final JsonCommand command) { @@ -545,10 +229,10 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ multiRow = false; } - validateDatatableName(datatableName); - EntityTables entityTable = resolveEntity(entityName); + datatableUtil.validateDatatableName(datatableName); + EntityTables entityTable = datatableUtil.resolveEntity(entityName); final boolean isConstraintApproach = this.configurationDomainService.isConstraintApproachEnabledForDatatables(); - final String fkColumnName = getFKField(entityTable); + final String fkColumnName = datatableUtil.getFKField(entityTable); final String dataTableNameAlias = datatableName.toLowerCase().replaceAll("\\s", "_"); final String fkName = dataTableNameAlias + "_" + fkColumnName; StringBuilder sqlBuilder = new StringBuilder(); @@ -637,41 +321,6 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withResourceIdAsString(datatableName).build(); } - private JsonElement addColumn(final String name, final JdbcJavaType dataType, final boolean isMandatory, final Integer length, - final boolean isUnique, final boolean isIndexed) { - JsonObject column = new JsonObject(); - column.addProperty(API_FIELD_NAME, name); - column.addProperty(API_FIELD_TYPE, dataType.formatSql(databaseTypeResolver.databaseType())); - if (dataType.isStringType()) { - column.addProperty(API_FIELD_LENGTH, length); - } - column.addProperty(API_FIELD_MANDATORY, Boolean.toString(isMandatory)); - column.addProperty(API_FIELD_UNIQUE, Boolean.toString(isUnique)); - column.addProperty(API_FIELD_INDEXED, Boolean.toString(isIndexed)); - return column; - } - - private void createFkIndex(String datatableName, String fkColumnName) { - String indexName = datatableKeywordGenerator.generateIndexName(datatableName, fkColumnName); - createIndex(indexName, datatableName, fkColumnName); - } - - private void createIndexesForTable(String datatableName, JsonArray columns) { - for (final JsonElement column : columns) { - createIndexForColumn(datatableName, column.getAsJsonObject()); - } - } - - private void createIndexForColumn(String datatableName, JsonObject column) { - String name = column.has(API_FIELD_NAME) ? column.get(API_FIELD_NAME).getAsString() : null; - final boolean unique = column.has(API_FIELD_UNIQUE) && column.get(API_FIELD_UNIQUE).getAsBoolean(); - final boolean indexed = column.has(API_FIELD_INDEXED) && column.get(API_FIELD_INDEXED).getAsBoolean(); - if (!unique && indexed) { - String indexName = datatableKeywordGenerator.generateIndexName(datatableName, name); - createIndex(indexName, datatableName, name); - } - } - @Transactional @Override public void updateDatatable(final String datatableName, final JsonCommand command) { @@ -686,7 +335,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ final String entityName = this.fromJsonHelper.extractStringNamed(API_PARAM_APPTABLE_NAME, element); final String entitySubType = this.fromJsonHelper.extractStringNamed(API_PARAM_SUBTYPE, element); - validateDatatableName(datatableName); + datatableUtil.validateDatatableName(datatableName); int rowCount = getDatatableRowCount(datatableName); final List columnHeaderData = this.genericDataService.fillResultsetColumnHeaders(datatableName); final Map mapColumnNameDefinition = searchUtil.mapHeadersToName(columnHeaderData); @@ -699,11 +348,11 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } if (!StringUtils.isBlank(entityName)) { - EntityTables entityTable = resolveEntity(entityName); - EntityTables oldEntityTable = queryForApplicationEntity(datatableName); + EntityTables entityTable = datatableUtil.resolveEntity(entityName); + EntityTables oldEntityTable = datatableUtil.queryForApplicationEntity(datatableName); if (entityTable != oldEntityTable) { - final String oldFKName = getFKField(oldEntityTable); - final String newFKName = getFKField(entityTable); + final String oldFKName = datatableUtil.getFKField(oldEntityTable); + final String newFKName = datatableUtil.getFKField(entityTable); final String oldConstraintName = datatableName.toLowerCase().replaceAll("\\s", "_") + "_" + oldFKName; final String newConstraintName = datatableName.toLowerCase().replaceAll("\\s", "_") + "_" + newFKName; StringBuilder sqlBuilder = new StringBuilder(); @@ -871,6 +520,216 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } } + @Transactional + @Override + public void deleteDatatable(final String datatableName) { + try { + this.context.authenticatedUser(); + datatableUtil.validateDatatableName(datatableName); + assertDataTableEmpty(datatableName); + deregisterDatatable(datatableName); + String[] sqlArray; + if (this.configurationDomainService.isConstraintApproachEnabledForDatatables()) { + final String deleteColumnCodeSql = "delete from x_table_column_code_mappings where column_alias_name like'" + + datatableName.toLowerCase().replaceAll("\\s", "_") + "_%'"; + sqlArray = new String[2]; + sqlArray[1] = deleteColumnCodeSql; + } else { + sqlArray = new String[1]; + } + final String sql = "DROP TABLE " + sqlGenerator.escape(datatableName); + sqlArray[0] = sql; + this.jdbcTemplate.batchUpdate(sqlArray); + } catch (final JpaSystemException | DataIntegrityViolationException e) { + final Throwable realCause = e.getCause(); + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource(RESOURCE_DATATABLE); + if (realCause.getMessage().contains("Unknown table")) { + baseDataValidator.reset().parameter(API_PARAM_DATATABLE_NAME).failWithCode(DOES_NOT_EXIST); + } + baseDataValidator.throwValidationErrors(); + } + } + + @Transactional + @Override + public CommandProcessingResult createNewDatatableEntry(final String dataTableName, final Long appTableId, final JsonCommand command) { + return createNewDatatableEntry(dataTableName, appTableId, command.json(), false); + } + + @Transactional + @Override + public CommandProcessingResult createNewDatatableEntry(final String dataTableName, final Long appTableId, final String json) { + return createNewDatatableEntry(dataTableName, appTableId, json, false); + } + + @Transactional + @Override + public CommandProcessingResult createPPIEntry(final String dataTableName, final Long appTableId, final JsonCommand command) { + return createNewDatatableEntry(dataTableName, appTableId, command.json(), true); + } + + @Transactional + @Override + public CommandProcessingResult updateDatatableEntryOneToOne(final String dataTableName, final Long appTableId, + final JsonCommand command) { + return updateDatatableEntry(dataTableName, appTableId, null, command); + } + + @Transactional + @Override + public CommandProcessingResult updateDatatableEntryOneToMany(final String dataTableName, final Long appTableId, final Long datatableId, + final JsonCommand command) { + return updateDatatableEntry(dataTableName, appTableId, datatableId, command); + } + + @Transactional + @Override + public CommandProcessingResult deleteDatatableEntries(final String dataTableName, final Long appTableId, JsonCommand command) { + return deleteDatatableEntries(dataTableName, appTableId, null, command); + } + + @Transactional + @Override + public CommandProcessingResult deleteDatatableEntry(final String dataTableName, final Long appTableId, final Long datatableId, + JsonCommand command) { + return deleteDatatableEntries(dataTableName, appTableId, datatableId, command); + } + + private void registerDataTable(final String entityName, final String dataTableName, final String entitySubType, final Integer category, + final String permissionsSql) { + datatableUtil.resolveEntity(entityName); + datatableUtil.validateDatatableName(dataTableName); + validateDataTableExists(dataTableName); + + Map paramMap = new HashMap<>(3); + final String registerDatatableSql = "insert into x_registered_table " + + "(registered_table_name, application_table_name, entity_subtype, category) " + + "values (:dataTableName, :applicationTableName, :entitySubType, :category)"; + paramMap.put("dataTableName", dataTableName); + paramMap.put("applicationTableName", entityName); + paramMap.put("entitySubType", entitySubType); + paramMap.put("category", category); + + try { + this.namedParameterJdbcTemplate.update(registerDatatableSql, paramMap); + this.jdbcTemplate.update(permissionsSql); + + // add the registered table to the config if it is a ppi + if (category.equals(DataTableApiConstant.CATEGORY_PPI)) { + this.namedParameterJdbcTemplate + .update("insert into c_configuration (name, value, enabled ) values( :dataTableName, '0', false)", paramMap); + } + + } catch (final JpaSystemException | DataIntegrityViolationException dve) { + handleDataIntegrityIssues(dataTableName, null, dve.getMostSpecificCause(), dve); + } catch (final PersistenceException dve) { + handleDataIntegrityIssues(dataTableName, null, ExceptionUtils.getRootCause(dve.getCause()), dve); + + } + } + + private String getPermissionSql(final String dataTableName) { + final String createPermission = "'CREATE_" + dataTableName + "'"; + final String createPermissionChecker = "'CREATE_" + dataTableName + "_CHECKER'"; + final String readPermission = "'READ_" + dataTableName + "'"; + final String updatePermission = "'UPDATE_" + dataTableName + "'"; + final String updatePermissionChecker = "'UPDATE_" + dataTableName + "_CHECKER'"; + final String deletePermission = "'DELETE_" + dataTableName + "'"; + final String deletePermissionChecker = "'DELETE_" + dataTableName + "_CHECKER'"; + final List escapedColumns = Stream.of("grouping", "code", "action_name", "entity_name", "can_maker_checker") + .map(sqlGenerator::escape).toList(); + final String columns = String.join(", ", escapedColumns); + + return "insert into m_permission (" + columns + ") values " + "('datatable', " + createPermission + ", 'CREATE', '" + dataTableName + + "', true)," + "('datatable', " + createPermissionChecker + ", 'CREATE', '" + dataTableName + "', false)," + + "('datatable', " + readPermission + ", 'READ', '" + dataTableName + "', false)," + "('datatable', " + updatePermission + + ", 'UPDATE', '" + dataTableName + "', true)," + "('datatable', " + updatePermissionChecker + ", 'UPDATE', '" + + dataTableName + "', false)," + "('datatable', " + deletePermission + ", 'DELETE', '" + dataTableName + "', true)," + + "('datatable', " + deletePermissionChecker + ", 'DELETE', '" + dataTableName + "', false)"; + } + + private Integer getCategory(final JsonCommand command) { + Integer category = command.integerValueOfParameterNamedDefaultToNullIfZero(DataTableApiConstant.categoryParamName); + return category == null ? DataTableApiConstant.CATEGORY_DEFAULT : category; + } + + private JsonElement addColumn(final String name, final JdbcJavaType dataType, final boolean isMandatory, final Integer length, + final boolean isUnique, final boolean isIndexed) { + JsonObject column = new JsonObject(); + column.addProperty(API_FIELD_NAME, name); + column.addProperty(API_FIELD_TYPE, dataType.formatSql(databaseTypeResolver.databaseType())); + if (dataType.isStringType()) { + column.addProperty(API_FIELD_LENGTH, length); + } + column.addProperty(API_FIELD_MANDATORY, Boolean.toString(isMandatory)); + column.addProperty(API_FIELD_UNIQUE, Boolean.toString(isUnique)); + column.addProperty(API_FIELD_INDEXED, Boolean.toString(isIndexed)); + return column; + } + + private void parseDatatableColumnObjectForCreate(final JsonObject column, StringBuilder sqlBuilder, + final StringBuilder constrainBuilder, final String dataTableNameAlias, final Map codeMappings, + final boolean isConstraintApproach) { + String name = column.has(API_FIELD_NAME) ? column.get(API_FIELD_NAME).getAsString() : null; + final String type = column.has(API_FIELD_TYPE) ? column.get(API_FIELD_TYPE).getAsString().toLowerCase() : null; + final Integer length = column.has(API_FIELD_LENGTH) ? column.get(API_FIELD_LENGTH).getAsInt() : null; + final boolean mandatory = column.has(API_FIELD_MANDATORY) && column.get(API_FIELD_MANDATORY).getAsBoolean(); + final boolean unique = column.has(API_FIELD_UNIQUE) && column.get(API_FIELD_UNIQUE).getAsBoolean(); + final String code = column.has(API_FIELD_CODE) ? column.get(API_FIELD_CODE).getAsString() : null; + + if (StringUtils.isNotBlank(code)) { + if (isConstraintApproach) { + codeMappings.put(dataTableNameAlias + "_" + name, this.codeReadPlatformService.retriveCode(code).getId()); + String fkName = "fk_" + dataTableNameAlias + "_" + name; + constrainBuilder.append(", CONSTRAINT ").append(sqlGenerator.escape(fkName)).append(" ").append("FOREIGN KEY (") + .append(sqlGenerator.escape(name)).append(") ").append(REFERENCES_CLAUSE) + .append(sqlGenerator.escape(CODE_VALUES_TABLE)).append(" (id)"); + } else { + name = datatableColumnNameToCodeValueName(name, code); + } + } + sqlBuilder.append(sqlGenerator.escape(name)); + if (type != null) { + sqlBuilder.append(" ").append(mapApiTypeToDbType(type, length)); + } + + if (unique) { + String uniqueKeyName = datatableKeywordGenerator.generateUniqueKeyName(dataTableNameAlias, name); + constrainBuilder.append(", CONSTRAINT ").append(sqlGenerator.escape(uniqueKeyName)).append(" ").append("UNIQUE (") + .append(sqlGenerator.escape(name)).append(")"); + } + + if (mandatory) { + sqlBuilder.append(NOT_NULL_CLAUSE); + } else { + sqlBuilder.append(DEFAULT_NULL); + } + + sqlBuilder.append(", "); + } + + private void createFkIndex(String datatableName, String fkColumnName) { + String indexName = datatableKeywordGenerator.generateIndexName(datatableName, fkColumnName); + createIndex(indexName, datatableName, fkColumnName); + } + + private void createIndexesForTable(String datatableName, JsonArray columns) { + for (final JsonElement column : columns) { + createIndexForColumn(datatableName, column.getAsJsonObject()); + } + } + + private void createIndexForColumn(String datatableName, JsonObject column) { + String name = column.has(API_FIELD_NAME) ? column.get(API_FIELD_NAME).getAsString() : null; + final boolean unique = column.has(API_FIELD_UNIQUE) && column.get(API_FIELD_UNIQUE).getAsBoolean(); + final boolean indexed = column.has(API_FIELD_INDEXED) && column.get(API_FIELD_INDEXED).getAsBoolean(); + if (!unique && indexed) { + String indexName = datatableKeywordGenerator.generateIndexName(datatableName, name); + createIndex(indexName, datatableName, name); + } + } + private void parseDatatableColumnForAdd(final JsonObject column, StringBuilder sqlBuilder, final String dataTableNameAlias, final StringBuilder constrainBuilder, final Map codeMappings, final boolean isConstraintApproach) { @@ -1243,59 +1102,10 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ jdbcTemplate.execute(sqlIndexUpdateBuilder.toString()); } - @Transactional - @Override - public void deleteDatatable(final String datatableName) { - try { - this.context.authenticatedUser(); - validateDatatableName(datatableName); - assertDataTableEmpty(datatableName); - deregisterDatatable(datatableName); - String[] sqlArray; - if (this.configurationDomainService.isConstraintApproachEnabledForDatatables()) { - final String deleteColumnCodeSql = "delete from x_table_column_code_mappings where column_alias_name like'" - + datatableName.toLowerCase().replaceAll("\\s", "_") + "_%'"; - sqlArray = new String[2]; - sqlArray[1] = deleteColumnCodeSql; - } else { - sqlArray = new String[1]; - } - final String sql = "DROP TABLE " + sqlGenerator.escape(datatableName); - sqlArray[0] = sql; - this.jdbcTemplate.batchUpdate(sqlArray); - } catch (final JpaSystemException | DataIntegrityViolationException e) { - final Throwable realCause = e.getCause(); - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource(RESOURCE_DATATABLE); - if (realCause.getMessage().contains("Unknown table")) { - baseDataValidator.reset().parameter(API_PARAM_DATATABLE_NAME).failWithCode(DOES_NOT_EXIST); - } - baseDataValidator.throwValidationErrors(); - } - } - - @Transactional - @Override - public CommandProcessingResult createNewDatatableEntry(final String dataTableName, final Long appTableId, final JsonCommand command) { - return createNewDatatableEntry(dataTableName, appTableId, command.json(), false); - } - - @Transactional - @Override - public CommandProcessingResult createNewDatatableEntry(final String dataTableName, final Long appTableId, final String json) { - return createNewDatatableEntry(dataTableName, appTableId, json, false); - } - - @Transactional - @Override - public CommandProcessingResult createPPIEntry(final String dataTableName, final Long appTableId, final JsonCommand command) { - return createNewDatatableEntry(dataTableName, appTableId, command.json(), true); - } - private CommandProcessingResult createNewDatatableEntry(final String dataTableName, final Long appTableId, final String json, boolean addScore) { - final EntityTables entityTable = queryForApplicationEntity(dataTableName); - CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(entityTable, appTableId); + final EntityTables entityTable = datatableUtil.queryForApplicationEntity(dataTableName); + CommandProcessingResult commandProcessingResult = datatableUtil.checkMainResourceExistsWithinScope(entityTable, appTableId); List columnHeaders = genericDataService.fillResultsetColumnHeaders(dataTableName); Map headersByName = searchUtil.mapHeadersToName(columnHeaders); @@ -1358,7 +1168,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } Long resourceId = appTableId; - if (isMultirowDatatable(columnHeaders)) { + if (datatableUtil.isMultirowDatatable(columnHeaders)) { resourceId = sqlGenerator.fetchPK(keyHolder); } @@ -1393,28 +1203,14 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ && !entityTable.getForeignKeyColumnNameOnDatatable().equals(columnName); } - @Transactional - @Override - public CommandProcessingResult updateDatatableEntryOneToOne(final String dataTableName, final Long appTableId, - final JsonCommand command) { - return updateDatatableEntry(dataTableName, appTableId, null, command); - } - - @Transactional - @Override - public CommandProcessingResult updateDatatableEntryOneToMany(final String dataTableName, final Long appTableId, final Long datatableId, - final JsonCommand command) { - return updateDatatableEntry(dataTableName, appTableId, datatableId, command); - } - @SuppressWarnings({ "WhitespaceAround" }) private CommandProcessingResult updateDatatableEntry(final String dataTableName, final Long appTableId, final Long datatableId, final JsonCommand command) { - final EntityTables entityTable = queryForApplicationEntity(dataTableName); - CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(entityTable, appTableId); + final EntityTables entityTable = datatableUtil.queryForApplicationEntity(dataTableName); + CommandProcessingResult commandProcessingResult = datatableUtil.checkMainResourceExistsWithinScope(entityTable, appTableId); - final GenericResultsetData existingRows = retrieveDataTableGenericResultSet(entityTable, dataTableName, appTableId, null, - datatableId); + final GenericResultsetData existingRows = datatableUtil.retrieveDataTableGenericResultSet(entityTable, dataTableName, appTableId, + null, datatableId); if (existingRows.hasNoEntries()) { throw new DatatableNotFoundException(dataTableName, appTableId); } @@ -1424,7 +1220,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } List columnHeaders = existingRows.getColumnHeaders(); - if (isMultirowDatatable(columnHeaders) && datatableId == null) { + if (datatableUtil.isMultirowDatatable(columnHeaders) && datatableId == null) { throw new PlatformDataIntegrityException("error.msg.attempting.multiple.update", "Application table: " + dataTableName + " Foreign key id: " + appTableId); } @@ -1504,32 +1300,19 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ return isUserInsertable(entityTable, columnHeader); } - @Transactional - @Override - public CommandProcessingResult deleteDatatableEntries(final String dataTableName, final Long appTableId, JsonCommand command) { - return deleteDatatableEntries(dataTableName, appTableId, null, command); - } - - @Transactional - @Override - public CommandProcessingResult deleteDatatableEntry(final String dataTableName, final Long appTableId, final Long datatableId, - JsonCommand command) { - return deleteDatatableEntries(dataTableName, appTableId, datatableId, command); - } - private CommandProcessingResult deleteDatatableEntries(final String dataTableName, final Long appTableId, final Long datatableId, JsonCommand command) { - validateDatatableName(dataTableName); + datatableUtil.validateDatatableName(dataTableName); if (isDatatableAttachedToEntityDatatableCheck(dataTableName)) { throw new DatatableEntryRequiredException(dataTableName, appTableId); } - final EntityTables entityTable = queryForApplicationEntity(dataTableName); - final CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(entityTable, appTableId); + final EntityTables entityTable = datatableUtil.queryForApplicationEntity(dataTableName); + final CommandProcessingResult commandProcessingResult = datatableUtil.checkMainResourceExistsWithinScope(entityTable, appTableId); String whereColumn; Long whereValue; if (datatableId == null) { - whereColumn = getFKField(entityTable); + whereColumn = datatableUtil.getFKField(entityTable); whereValue = appTableId; } else { whereColumn = TABLE_FIELD_ID; @@ -1556,147 +1339,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ .build(); } - @Override - @Transactional(readOnly = true) - public GenericResultsetData retrieveDataTableGenericResultSet(final String dataTableName, final Long appTableId, final String order, - final Long id) { - final EntityTables entityTable = queryForApplicationEntity(dataTableName); - checkMainResourceExistsWithinScope(entityTable, appTableId); - return retrieveDataTableGenericResultSet(entityTable, dataTableName, appTableId, order, id); - } - - private GenericResultsetData retrieveDataTableGenericResultSet(final EntityTables entityTable, final String dataTableName, - final Long appTableId, final String order, final Long id) { - final List columnHeaders = this.genericDataService.fillResultsetColumnHeaders(dataTableName); - final boolean multiRow = isMultirowDatatable(columnHeaders); - - String whereClause = getFKField(entityTable) + " = " + appTableId; - sqlValidator.validate(whereClause); - String sql = "select * from " + sqlGenerator.escape(dataTableName) + " where " + whereClause; - - // id only used for reading a specific entry that belongs to appTableId (in a one to many datatable) - if (multiRow && id != null) { - sql = sql + " and " + TABLE_FIELD_ID + " = " + id; - } - if (StringUtils.isNotBlank(order)) { - this.columnValidator.validateSqlInjection(sql, order); - sql = sql + " order by " + order; - } - - final List result = genericDataService.fillResultsetRowData(sql, columnHeaders); - return new GenericResultsetData(columnHeaders, result); - } - - private CommandProcessingResult checkMainResourceExistsWithinScope(@NotNull EntityTables entityTable, final Long appTableId) { - final String sql = dataScopedSQL(entityTable, appTableId); - log.debug("data scoped sql: {}", sql); - final SqlRowSet rs = this.jdbcTemplate.queryForRowSet(sql); - - if (!rs.next()) { - throw new DatatableNotFoundException(entityTable, appTableId); - } - final Long officeId = (Long) rs.getObject("officeId"); - final Long groupId = (Long) rs.getObject("groupId"); - final Long clientId = (Long) rs.getObject("clientId"); - final Long savingsId = (Long) rs.getObject("savingsId"); - final Long loanId = (Long) rs.getObject("loanId"); - final Long transactionId = (Long) rs.getObject("transactionId"); - final Long entityId = (Long) rs.getObject("entityId"); - - if (rs.next()) { - throw new DatatableSystemErrorException("System Error: More than one row returned from data scoping query"); - } - - return new CommandProcessingResultBuilder() // - .withOfficeId(officeId) // - .withGroupId(groupId) // - .withClientId(clientId) // - .withSavingsId(savingsId) // - .withLoanId(loanId).withTransactionId(transactionId == null ? null : String.valueOf(transactionId)).withEntityId(entityId)// - .build(); - } - - private String dataScopedSQL(@NotNull EntityTables entityTable, final Long appTableId) { - // unfortunately have to, one way or another, be able to restrict data to the users office hierarchy. Here, a - // few key tables are done. But if additional fields are needed on other tables the same pattern applies - final AppUser currentUser = this.context.authenticatedUser(); - String officeHierarchy = currentUser.getOffice().getHierarchy(); - // m_loan and m_savings_account are connected to an m_office through either an m_client or an m_group If both it - // means it relates to an m_client that is in a group (still an m_client account) - return switch (entityTable) { - case LOAN -> "select distinct x.* from ( " - + "(select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId, null as transactionId, null as entityId from m_loan l " - + getClientOfficeJoinCondition(officeHierarchy, "l") + " where l.id = " + appTableId + ")" + " union all " - + "(select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId, null as transactionId, null as entityId from m_loan l " - + getGroupOfficeJoinCondition(officeHierarchy, "l") + " where l.id = " + appTableId + ")" + " ) as x"; - case SAVINGS -> "select distinct x.* from ( " - + "(select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, null as transactionId, null as entityId " - + "from m_savings_account s " + getClientOfficeJoinCondition(officeHierarchy, "s") + " where s.id = " + appTableId + ")" - + " union all " - + "(select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, null as transactionId, null as entityId " - + "from m_savings_account s " + getGroupOfficeJoinCondition(officeHierarchy, "s") + " where s.id = " + appTableId + ")" - + " ) as x"; - case SAVINGS_TRANSACTION -> "select distinct x.* from ( " - + "(select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, t.id as transactionId, null as entityId " - + "from m_savings_account_transaction t join m_savings_account s on t.savings_account_id = s.id " - + getClientOfficeJoinCondition(officeHierarchy, "s") + " where t.id = " + appTableId + ")" + " union all " - + "(select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, t.id as transactionId, null as entityId " - + "from m_savings_account_transaction t join m_savings_account s on t.savings_account_id = s.id " - + getGroupOfficeJoinCondition(officeHierarchy, "s") + " where t.id = " + appTableId + ")" + " ) as x"; - case CLIENT -> - "select o.id as officeId, null as groupId, c.id as clientId, null as savingsId, null as loanId, null as transactionId, null as entityId from m_client c " - + getOfficeJoinCondition(officeHierarchy, "c") + " where c.id = " + appTableId; - case GROUP, CENTER -> - "select o.id as officeId, g.id as groupId, null as clientId, null as savingsId, null as loanId, null as transactionId, null as entityId from m_group g " - + getOfficeJoinCondition(officeHierarchy, "g") + " where g.id = " + appTableId; - case OFFICE -> - "select o.id as officeId, null as groupId, null as clientId, null as savingsId, null as loanId, null as transactionId, null as entityId from m_office o " - + "where o.hierarchy like '" + officeHierarchy + "%'" + " and o.id = " + appTableId; - case LOAN_PRODUCT, SAVINGS_PRODUCT, SHARE_PRODUCT -> - "select null as officeId, null as groupId, null as clientId, null as savingsId, null as loanId, null as transactionId, p.id as entityId from " - + entityTable.getName() + " as p WHERE p.id = " + appTableId; - default -> throw new PlatformDataIntegrityException("error.msg.invalid.dataScopeCriteria", - "Application Table: " + entityTable.getName() + " not catered for in data Scoping"); - }; - } - - private String getClientOfficeJoinCondition(String officeHierarchy, String appTableAlias) { - return " join m_client c on c.id = " + appTableAlias + ".client_id " + getOfficeJoinCondition(officeHierarchy, "c"); - } - - private String getGroupOfficeJoinCondition(String officeHierarchy, String appTableAlias) { - return " join m_group g on g.id = " + appTableAlias + ".group_id " + getOfficeJoinCondition(officeHierarchy, "g"); - } - - private String getOfficeJoinCondition(String officeHierarchy, String joinTableAlias) { - return " join m_office o on o.id = " + joinTableAlias + ".office_id and o.hierarchy like '" + officeHierarchy + "%' "; - } - - @NotNull - private EntityTables queryForApplicationEntity(final String datatable) { - sqlValidator.validate(datatable); - final String sql = "SELECT application_table_name FROM x_registered_table where registered_table_name = ?"; - final SqlRowSet rowSet = jdbcTemplate.queryForRowSet(sql, datatable); // NOSONAR - - String applicationTableName; - if (rowSet.next()) { - applicationTableName = rowSet.getString(APPLICATION_TABLE_NAME); - } else { - throw new DatatableNotFoundException(datatable); - } - return resolveEntity(applicationTableName); - } - - @Override - public Long countDatatableEntries(final String datatableName, final Long appTableId, String foreignKeyColumn) { - final String sqlString = "SELECT COUNT(" + sqlGenerator.escape(foreignKeyColumn) + ") FROM " + sqlGenerator.escape(datatableName) - + " WHERE " + sqlGenerator.escape(foreignKeyColumn) + " = " + appTableId; - return this.jdbcTemplate.queryForObject(sqlString, Long.class); // NOSONAR - } - - // --- Validation --- - - public boolean isDatatableAttachedToEntityDatatableCheck(final String datatableName) { + private boolean isDatatableAttachedToEntityDatatableCheck(final String datatableName) { String sql = "SELECT COUNT(edc.x_registered_table_name) FROM x_registered_table xrt" + " JOIN m_entity_datatable_check edc ON edc.x_registered_table_name = xrt.registered_table_name" + " WHERE edc.x_registered_table_name = '" + datatableName + "'"; @@ -1704,38 +1347,6 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ return count != null && count > 0; } - private EntityTables resolveEntity(final String entityName) { - EntityTables entityTable = EntityTables.fromEntityName(entityName); - if (entityTable == null) { - throw new PlatformDataIntegrityException("error.msg.invalid.application.table", "Invalid Datatable entity: " + entityName, - API_FIELD_NAME, entityName); - } - return entityTable; - } - - private void validateDatatableName(final String name) { - if (name == null || name.isEmpty()) { - throw new PlatformDataIntegrityException("error.msg.datatables.datatable.null.name", "Data table name must not be blank."); - } else if (!name.matches(DATATABLE_NAME_REGEX_PATTERN)) { - throw new PlatformDataIntegrityException("error.msg.datatables.datatable.invalid.name.regex", "Invalid data table name.", name); - } - sqlValidator.validate(name); - } - - private String validateDatatableRegistered(String datatable) { - validateDatatableName(datatable); - if (!isRegisteredDataTable(datatable)) { - throw new DatatableNotFoundException(datatable); - } - return datatable; - } - - private boolean isRegisteredDataTable(final String datatable) { - final String sql = "SELECT COUNT(application_table_name) FROM " + TABLE_REGISTERED_TABLE + " WHERE registered_table_name = ?"; - final Integer count = jdbcTemplate.queryForObject(sql, Integer.class, datatable); - return count != null && count > 0; - } - private void validateDataTableExists(final String datatableName) { final String sql = "select (CASE WHEN exists (select 1 from information_schema.tables where table_schema = " + sqlGenerator.currentSchema() + " and table_name = ?) THEN 'true' ELSE 'false' END)"; @@ -1777,20 +1388,10 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ return count == null ? 0 : count; } - // --- Utils --- - - private String getFKField(EntityTables entityTable) { - return entityTable.getForeignKeyColumnNameOnDatatable(); - } - private static boolean isTechnicalParam(String param) { return API_PARAM_DATE_FORMAT.equals(param) || API_PARAM_DATETIME_FORMAT.equals(param) || API_PARAM_LOCALE.equals(param); } - private boolean isMultirowDatatable(final List columnHeaders) { - return searchUtil.findFiltered(columnHeaders, e -> e.isNamed(TABLE_FIELD_ID)) != null; - } - private String datatableColumnNameToCodeValueName(final String columnName, final String code) { return code + "_cd_" + columnName; } @@ -1827,4 +1428,5 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ log.error("Error occured.", e); throw ErrorHandler.getMappable(e, msgCode, msg, param, msgArgs); } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksReadPlatformServiceImpl.java index 1192e74f3..6915940df 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksReadPlatformServiceImpl.java @@ -30,7 +30,6 @@ import org.apache.fineract.infrastructure.core.service.Page; import org.apache.fineract.infrastructure.core.service.PaginationHelper; import org.apache.fineract.infrastructure.core.service.SearchParameters; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; -import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver; import org.apache.fineract.infrastructure.core.service.database.JdbcJavaType; import org.apache.fineract.infrastructure.core.service.database.SqlOperator; import org.apache.fineract.infrastructure.dataqueries.data.DatatableCheckStatusData; @@ -46,7 +45,6 @@ import org.apache.fineract.portfolio.loanproduct.data.LoanProductData; import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService; import org.apache.fineract.portfolio.savings.data.SavingsProductData; import org.apache.fineract.portfolio.savings.service.SavingsProductReadPlatformService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Service; @@ -55,33 +53,28 @@ import org.springframework.stereotype.Service; public class EntityDatatableChecksReadPlatformServiceImpl implements EntityDatatableChecksReadService { private final JdbcTemplate jdbcTemplate; - protected final DatabaseTypeResolver databaseTypeResolver; private final DatabaseSpecificSQLGenerator sqlGenerator; private final RegisterDataTableMapper registerDataTableMapper; private final EntityDataTableChecksMapper entityDataTableChecksMapper; private final EntityDatatableChecksRepository entityDatatableChecksRepository; - private final ReadWriteNonCoreDataService readWriteNonCoreDataService; + private final DatatableReadService datatableReadService; private final LoanProductReadPlatformService loanProductReadPlatformService; private final SavingsProductReadPlatformService savingsProductReadPlatformService; private final PaginationHelper paginationHelper; - @Autowired - public EntityDatatableChecksReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, DatabaseTypeResolver databaseTypeResolver, - DatabaseSpecificSQLGenerator sqlGenerator, final LoanProductReadPlatformService loanProductReadPlatformService, - final SavingsProductReadPlatformService savingsProductReadPlatformService, - final EntityDatatableChecksRepository entityDatatableChecksRepository, - final ReadWriteNonCoreDataService readWriteNonCoreDataService, PaginationHelper paginationHelper) { - + public EntityDatatableChecksReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, final DatabaseSpecificSQLGenerator sqlGenerator, + final EntityDatatableChecksRepository entityDatatableChecksRepository, final DatatableReadService datatableReadService, + final LoanProductReadPlatformService loanProductReadPlatformService, + final SavingsProductReadPlatformService savingsProductReadPlatformService, final PaginationHelper paginationHelper) { this.jdbcTemplate = jdbcTemplate; - this.databaseTypeResolver = databaseTypeResolver; this.sqlGenerator = sqlGenerator; - this.registerDataTableMapper = new RegisterDataTableMapper(); - this.entityDataTableChecksMapper = new EntityDataTableChecksMapper(); + this.entityDatatableChecksRepository = entityDatatableChecksRepository; + this.datatableReadService = datatableReadService; this.loanProductReadPlatformService = loanProductReadPlatformService; this.savingsProductReadPlatformService = savingsProductReadPlatformService; - this.entityDatatableChecksRepository = entityDatatableChecksRepository; - this.readWriteNonCoreDataService = readWriteNonCoreDataService; this.paginationHelper = paginationHelper; + this.registerDataTableMapper = new RegisterDataTableMapper(); + this.entityDataTableChecksMapper = new EntityDataTableChecksMapper(); } @Override @@ -134,7 +127,7 @@ public class EntityDatatableChecksReadPlatformServiceImpl implements EntityDatat if (tableRequiredBeforeAction != null && tableRequiredBeforeAction.size() > 0) { List ret = new ArrayList<>(); for (EntityDatatableChecks t : tableRequiredBeforeAction) { - ret.add(this.readWriteNonCoreDataService.retrieveDatatable(t.getDatatableName())); + ret.add(this.datatableReadService.retrieveDatatable(t.getDatatableName())); } return ret; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksWritePlatformServiceImpl.java index 99f006da6..1c419aa3f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksWritePlatformServiceImpl.java @@ -60,7 +60,8 @@ public class EntityDatatableChecksWritePlatformServiceImpl implements EntityData private final PlatformSecurityContext context; private final EntityDatatableChecksDataValidator fromApiJsonDeserializer; private final EntityDatatableChecksRepository entityDatatableChecksRepository; - private final ReadWriteNonCoreDataService readWriteNonCoreDataService; + private final DatatableReadService datatableReadService; + private final DatatableWriteService datatableWriteService; private final LoanProductReadPlatformService loanProductReadPlatformService; private final SavingsProductReadPlatformService savingsProductReadPlatformService; private final FromJsonHelper fromApiJsonHelper; @@ -77,7 +78,7 @@ public class EntityDatatableChecksWritePlatformServiceImpl implements EntityData // check if the datatable is linked to the entity String datatableName = command.stringValueOfParameterNamed("datatableName"); - DatatableData datatableData = this.readWriteNonCoreDataService.retrieveDatatable(datatableName); + DatatableData datatableData = this.datatableReadService.retrieveDatatable(datatableName); if (datatableData == null) { throw new DatatableNotFoundException(datatableName); @@ -154,7 +155,7 @@ public class EntityDatatableChecksWritePlatformServiceImpl implements EntityData for (EntityDatatableChecks t : tableRequiredBeforeClientActivation) { final String datatableName = t.getDatatableName(); - final Long countEntries = readWriteNonCoreDataService.countDatatableEntries(datatableName, entityId, foreignKeyColumn); + final Long countEntries = datatableReadService.countDatatableEntries(datatableName, entityId, foreignKeyColumn); log.debug("The are {} entries in the table {}", countEntries, datatableName); if (countEntries.intValue() == 0) { @@ -182,7 +183,7 @@ public class EntityDatatableChecksWritePlatformServiceImpl implements EntityData for (EntityDatatableChecks t : tableRequiredBeforAction) { final String datatableName = t.getDatatableName(); - final Long countEntries = readWriteNonCoreDataService.countDatatableEntries(datatableName, entityId, foreignKeyColumn); + final Long countEntries = datatableReadService.countDatatableEntries(datatableName, entityId, foreignKeyColumn); log.debug("The are {} entries in the table {}", countEntries, datatableName); if (countEntries.intValue() == 0) { @@ -220,7 +221,7 @@ public class EntityDatatableChecksWritePlatformServiceImpl implements EntityData isMakerCheckerEnabled = true; } try { - this.readWriteNonCoreDataService.createNewDatatableEntry(datatableName, entityId, datatableData.toString()); + datatableWriteService.createNewDatatableEntry(datatableName, entityId, datatableData.toString()); } catch (PlatformApiDataValidationException e) { for (ApiParameterError error : e.getErrors()) { error.setParameterName("datatables." + datatableName + "." + error.getParameterName()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/starter/DataQueriesAutoConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/starter/DataQueriesAutoConfiguration.java index 4de72b69a..40a21b4ed 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/starter/DataQueriesAutoConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/starter/DataQueriesAutoConfiguration.java @@ -26,13 +26,15 @@ import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecific import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver; import org.apache.fineract.infrastructure.dataqueries.data.DataTableValidator; import org.apache.fineract.infrastructure.dataqueries.service.DatatableKeywordGenerator; +import org.apache.fineract.infrastructure.dataqueries.service.DatatableReadService; +import org.apache.fineract.infrastructure.dataqueries.service.DatatableReadServiceImpl; +import org.apache.fineract.infrastructure.dataqueries.service.DatatableUtil; +import org.apache.fineract.infrastructure.dataqueries.service.DatatableWriteService; +import org.apache.fineract.infrastructure.dataqueries.service.DatatableWriteServiceImpl; import org.apache.fineract.infrastructure.dataqueries.service.GenericDataService; -import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; -import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataServiceImpl; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.service.SqlValidator; -import org.apache.fineract.infrastructure.security.utils.ColumnValidator; import org.apache.fineract.portfolio.search.service.SearchUtil; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; @@ -45,17 +47,27 @@ public class DataQueriesAutoConfiguration { @Bean @ConditionalOnMissingBean - public ReadWriteNonCoreDataService readWriteNonCoreDataService(final JdbcTemplate jdbcTemplate, - final DatabaseTypeResolver databaseTypeResolver, final DatabaseSpecificSQLGenerator sqlGenerator, - final PlatformSecurityContext context, final FromJsonHelper fromJsonHelper, final GenericDataService genericDataService, - final DatatableCommandFromApiJsonDeserializer fromApiJsonDeserializer, - final ConfigurationDomainService configurationDomainService, final CodeReadPlatformService codeReadPlatformService, - final DataTableValidator dataTableValidator, final ColumnValidator columnValidator, - final NamedParameterJdbcTemplate namedParameterJdbcTemplate, DatatableKeywordGenerator datatableKeywordGenerator, - SqlValidator sqlValidator, SearchUtil searchUtil, final BusinessEventNotifierService businessEventNotifierService) { - return new ReadWriteNonCoreDataServiceImpl(jdbcTemplate, databaseTypeResolver, sqlGenerator, context, fromJsonHelper, - genericDataService, fromApiJsonDeserializer, configurationDomainService, codeReadPlatformService, dataTableValidator, - columnValidator, namedParameterJdbcTemplate, datatableKeywordGenerator, sqlValidator, searchUtil, - businessEventNotifierService); + public DatatableReadService datatableReadService(final JdbcTemplate jdbcTemplate, final DatabaseSpecificSQLGenerator sqlGenerator, + final PlatformSecurityContext context, final GenericDataService genericDataService, final DataTableValidator dataTableValidator, + final SqlValidator sqlValidator, final SearchUtil searchUtil, final DatatableUtil datatableUtil) { + return new DatatableReadServiceImpl(jdbcTemplate, sqlGenerator, context, genericDataService, dataTableValidator, sqlValidator, + searchUtil, datatableUtil); } + + @Bean + @ConditionalOnMissingBean + public DatatableWriteService datatableWriteService(final JdbcTemplate jdbcTemplate, final DatabaseTypeResolver databaseTypeResolver, + final DatabaseSpecificSQLGenerator sqlGenerator, final PlatformSecurityContext context, final FromJsonHelper fromJsonHelper, + final GenericDataService genericDataService, final DatatableCommandFromApiJsonDeserializer fromApiJsonDeserializer, + final ConfigurationDomainService configurationDomainService, final CodeReadPlatformService codeReadPlatformService, + final DataTableValidator dataTableValidator, final NamedParameterJdbcTemplate namedParameterJdbcTemplate, + final DatatableKeywordGenerator datatableKeywordGenerator, final SearchUtil searchUtil, + final BusinessEventNotifierService businessEventNotifierService, final DatatableReadService datatableReadService, + final DatatableUtil datatableUtil) { + return new DatatableWriteServiceImpl(jdbcTemplate, databaseTypeResolver, sqlGenerator, context, fromJsonHelper, genericDataService, + fromApiJsonDeserializer, configurationDomainService, codeReadPlatformService, dataTableValidator, + namedParameterJdbcTemplate, datatableKeywordGenerator, searchUtil, businessEventNotifierService, datatableReadService, + datatableUtil); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/survey/service/ReadSurveyServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/survey/service/ReadSurveyServiceImpl.java index ab4c89f62..2b8c45d94 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/survey/service/ReadSurveyServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/survey/service/ReadSurveyServiceImpl.java @@ -26,8 +26,8 @@ import org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant; import org.apache.fineract.infrastructure.dataqueries.data.DatatableData; import org.apache.fineract.infrastructure.dataqueries.data.GenericResultsetData; import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData; +import org.apache.fineract.infrastructure.dataqueries.service.DatatableReadService; import org.apache.fineract.infrastructure.dataqueries.service.GenericDataService; -import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.service.SqlValidator; import org.apache.fineract.infrastructure.survey.data.ClientScoresOverview; @@ -46,7 +46,7 @@ public class ReadSurveyServiceImpl implements ReadSurveyService { private final JdbcTemplate jdbcTemplate; private final SqlValidator sqlValidator; private final GenericDataService genericDataService; - private final ReadWriteNonCoreDataService readWriteNonCoreDataService; + private final DatatableReadService datatableReadService; @Override public List retrieveAllSurveys() { @@ -183,7 +183,7 @@ public class ReadSurveyServiceImpl implements ReadSurveyService { @Override public GenericResultsetData retrieveSurveyEntry(String surveyName, Long clientId, Long entryId) { - return readWriteNonCoreDataService.retrieveDataTableGenericResultSet(surveyName, clientId, null, entryId); + return datatableReadService.retrieveDataTableGenericResultSet(surveyName, clientId, null, entryId); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/survey/service/WriteSurveyServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/survey/service/WriteSurveyServiceImpl.java index 06d553b48..8b6135f32 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/survey/service/WriteSurveyServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/survey/service/WriteSurveyServiceImpl.java @@ -24,7 +24,8 @@ import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; -import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; +import org.apache.fineract.infrastructure.dataqueries.service.DatatableReadService; +import org.apache.fineract.infrastructure.dataqueries.service.DatatableWriteService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -35,16 +36,16 @@ import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor public class WriteSurveyServiceImpl implements WriteSurveyService { - private final ReadWriteNonCoreDataService readWriteNonCoreDataService; + private final DatatableReadService datatableReadService; + private final DatatableWriteService datatableWriteService; private final DatabaseSpecificSQLGenerator sqlGenerator; @Override @Transactional public CommandProcessingResult registerSurvey(JsonCommand command) { - - final String dataTableName = this.readWriteNonCoreDataService.getDataTableName(command.getUrl()); + final String dataTableName = datatableReadService.getDataTableName(command.getUrl()); final String permissionSql = this.getPermissionSql(dataTableName); - this.readWriteNonCoreDataService.registerDatatable(command, permissionSql); + datatableWriteService.registerDatatable(command, permissionSql); return CommandProcessingResult.commandOnlyResult(command.commandId()); @@ -73,8 +74,7 @@ public class WriteSurveyServiceImpl implements WriteSurveyService { @Transactional @Override public CommandProcessingResult fullFillSurvey(final String dataTableName, final Long appTableId, final JsonCommand command) { - - return readWriteNonCoreDataService.createPPIEntry(dataTableName, appTableId, command); + return datatableWriteService.createPPIEntry(dataTableName, appTableId, command); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionsSearchServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionsSearchServiceImpl.java index 6ccea902e..b954131df 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionsSearchServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionsSearchServiceImpl.java @@ -38,8 +38,8 @@ import org.apache.fineract.infrastructure.core.service.database.SqlOperator; import org.apache.fineract.infrastructure.dataqueries.data.DataTableValidator; import org.apache.fineract.infrastructure.dataqueries.data.EntityTables; import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData; +import org.apache.fineract.infrastructure.dataqueries.service.DatatableReadService; import org.apache.fineract.infrastructure.dataqueries.service.GenericDataService; -import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; @@ -66,7 +66,7 @@ public class SavingsAccountTransactionsSearchServiceImpl implements SavingsAccou private final PlatformSecurityContext context; private final GenericDataService genericDataService; private final DatabaseSpecificSQLGenerator sqlGenerator; - private final ReadWriteNonCoreDataService datatableService; + private final DatatableReadService datatableReadService; private final DataTableValidator dataTableValidator; private final JdbcTemplate jdbcTemplate; private final SearchUtil searchUtil; @@ -239,7 +239,7 @@ public class SavingsAccountTransactionsSearchServiceImpl implements SavingsAccou ArrayList dataParams = new ArrayList<>(); for (int i = 0; i < datatableQueries.size(); i++) { TableQueryData tableQuery = datatableQueries.get(i); - boolean added = datatableService.buildDataQueryEmbedded(EntityTables.SAVINGS_TRANSACTION, tableQuery.getTable(), + boolean added = datatableReadService.buildDataQueryEmbedded(EntityTables.SAVINGS_TRANSACTION, tableQuery.getTable(), tableQuery.getQuery(), selectColumns, dataSelect, dataFrom, dataWhere, dataParams, alias, ("d" + i), dateFormat, dateTimeFormat, locale); if (added) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java index 19578d472..e1bc759a7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java @@ -30,10 +30,10 @@ import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.PaginationHelper; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; import org.apache.fineract.infrastructure.dataqueries.data.DataTableValidator; +import org.apache.fineract.infrastructure.dataqueries.service.DatatableReadService; import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksReadService; import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksWritePlatformService; import org.apache.fineract.infrastructure.dataqueries.service.GenericDataService; -import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; import org.apache.fineract.infrastructure.entityaccess.service.FineractEntityAccessUtil; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; @@ -154,7 +154,7 @@ public class SavingsConfiguration { @Bean @ConditionalOnMissingBean(SavingsAccountTransactionSearchService.class) public SavingsAccountTransactionSearchService savingsAccountTransactionSearchService(PlatformSecurityContext context, - GenericDataService genericDataService, DatabaseSpecificSQLGenerator sqlGenerator, ReadWriteNonCoreDataService datatableService, + GenericDataService genericDataService, DatabaseSpecificSQLGenerator sqlGenerator, DatatableReadService datatableService, DataTableValidator dataTableValidator, JdbcTemplate jdbcTemplate, SearchUtil searchUtil) { return new SavingsAccountTransactionsSearchServiceImpl(context, genericDataService, sqlGenerator, datatableService, dataTableValidator, jdbcTemplate, searchUtil); diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableBusinessEventTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableBusinessEventTest.java index 13171e7d7..20dd8c06a 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableBusinessEventTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableBusinessEventTest.java @@ -18,11 +18,13 @@ */ package org.apache.fineract.infrastructure.dataqueries.service; -import static org.junit.Assert.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doNothing; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -36,66 +38,64 @@ import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.codes.service.CodeReadPlatformService; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.domain.ActionContext; import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; -import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenantConnection; import org.apache.fineract.infrastructure.core.serialization.DatatableCommandFromApiJsonDeserializer; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver; import org.apache.fineract.infrastructure.dataqueries.data.DataTableValidator; +import org.apache.fineract.infrastructure.dataqueries.data.EntityTables; +import org.apache.fineract.infrastructure.dataqueries.data.GenericResultsetData; +import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData; import org.apache.fineract.infrastructure.dataqueries.data.ResultsetRowData; import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.datatable.DatatableEntryBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; -import org.apache.fineract.infrastructure.security.utils.ColumnValidator; -import org.apache.fineract.infrastructure.security.utils.DefaultSqlValidator; import org.apache.fineract.organisation.office.domain.Office; import org.apache.fineract.portfolio.search.service.SearchUtil; import org.apache.fineract.useradministration.domain.AppUser; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; -import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.PreparedStatementCreator; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.KeyHolder; -import org.springframework.jdbc.support.rowset.SqlRowSet; @ExtendWith(MockitoExtension.class) -public class DatatableBusinessEventTest { +@MockitoSettings(strictness = Strictness.LENIENT) +class DatatableBusinessEventTest { + + private static final String DATATABLE_NAME = "test_loan_data"; + private static final long APP_TABLE_ID = 1L; + private static final long DATATABLE_ID = 1L; - @Mock - private PlatformSecurityContext context; - @Mock - private BusinessEventNotifierService businessEventNotifierService; @Mock private JdbcTemplate jdbcTemplate; @Mock - private GenericDataService genericDataService; - @Mock - private SearchUtil searchUtil; - @Mock private DatabaseTypeResolver databaseTypeResolver; @Mock private DatabaseSpecificSQLGenerator sqlGenerator; @Mock - private FineractPlatformTenantConnection tenantConnection; - @Mock - private DefaultSqlValidator sqlValidator; + private PlatformSecurityContext context; @Mock private FromJsonHelper fromJsonHelper; @Mock + private GenericDataService genericDataService; + @Mock private DatatableCommandFromApiJsonDeserializer fromApiJsonDeserializer; @Mock private ConfigurationDomainService configurationDomainService; @@ -104,38 +104,73 @@ public class DatatableBusinessEventTest { @Mock private DataTableValidator dataTableValidator; @Mock - private ColumnValidator columnValidator; - @Mock private NamedParameterJdbcTemplate namedParameterJdbcTemplate; @Mock private DatatableKeywordGenerator datatableKeywordGenerator; + @Mock + private SearchUtil searchUtil; + @Mock + private BusinessEventNotifierService businessEventNotifierService; + @Mock + private DatatableReadService datatableReadService; + @Mock + private DatatableUtil datatableUtil; @InjectMocks - private ReadWriteNonCoreDataServiceImpl underTest; - - @Captor - private ArgumentCaptor> businessEventArgumentCaptor; - - private static String DATATABLE_NAME = "test_loan_data"; + private DatatableWriteServiceImpl underTest; @BeforeEach - public void setUp() { + void setUp() { + setupThreadLocalContext(); + setupSecurityContext(); + setupEntityTable(); + setupCommandProcessingResult(); + } + + @AfterEach + void tearDown() { + ThreadLocalContextUtil.reset(); + } + + @Test + void shouldNotifyBusinessEventAfterCreatingNewDatatableEntry() { + setupCreateDatatableEntryMocks(); + JsonCommand command = createJsonCommand("{}", APP_TABLE_ID); + + underTest.createNewDatatableEntry(DATATABLE_NAME, APP_TABLE_ID, command); + + verifyBusinessEvent(APP_TABLE_ID); + } + + @Test + void shouldNotifyBusinessEventAfterUpdatingDatatableEntry() { + setupUpdateDeleteDatatableMocks(); + JsonCommand command = createJsonCommand("{}", APP_TABLE_ID); + + underTest.updateDatatableEntryOneToMany(DATATABLE_NAME, APP_TABLE_ID, DATATABLE_ID, command); + + verifyBusinessEvent(APP_TABLE_ID); + } + + @Test + void shouldNotifyBusinessEventAfterDeletingDatatableEntry() { + setupUpdateDeleteDatatableMocks(); + JsonCommand command = createJsonCommand("{}", APP_TABLE_ID); + + underTest.deleteDatatableEntry(DATATABLE_NAME, APP_TABLE_ID, DATATABLE_ID, command); + + verifyBusinessEvent(APP_TABLE_ID); + } + + private void setupThreadLocalContext() { ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); - ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, LocalDate.parse("2024-01-16"), - BusinessDateType.COB_DATE, LocalDate.parse("2024-01-15")))); - - SqlRowSet sqlRS = Mockito.mock(SqlRowSet.class); - SqlRowSet sqlRSData = Mockito.mock(SqlRowSet.class); - doNothing().when(sqlValidator).validate(anyString()); - when(jdbcTemplate.queryForRowSet(anyString(), anyString())).thenReturn(sqlRS); - when(jdbcTemplate.queryForRowSet(anyString())).thenReturn(sqlRSData); - - when(sqlRS.next()).thenReturn(true).thenReturn(false); - when(sqlRS.getString("application_table_name")).thenReturn("m_loan"); - when(sqlRSData.next()).thenReturn(true).thenReturn(false); - when(sqlRSData.getObject(anyString())).thenReturn(1L); + Map businessDates = Map.of(BusinessDateType.BUSINESS_DATE, LocalDate.parse("2024-01-16"), + BusinessDateType.COB_DATE, LocalDate.parse("2024-01-15")); + ThreadLocalContextUtil.setBusinessDates(new HashMap<>(businessDates)); + } + private void setupSecurityContext() { AppUser currentUser = Mockito.mock(AppUser.class); Office office = Mockito.mock(Office.class); when(context.authenticatedUser()).thenReturn(currentUser); @@ -143,61 +178,57 @@ public class DatatableBusinessEventTest { when(office.getHierarchy()).thenReturn("."); } - @AfterEach - public void tearDown() { - ThreadLocalContextUtil.reset(); + private void setupEntityTable() { + EntityTables entityTable = Mockito.mock(EntityTables.class); + when(entityTable.getForeignKeyColumnNameOnDatatable()).thenReturn("loan_id"); + when(datatableUtil.queryForApplicationEntity(anyString())).thenReturn(entityTable); } - @Test - public void businessEventCreateNewDatatableEntryTest() { + private void setupCommandProcessingResult() { + CommandProcessingResult commandProcessingResult = Mockito.mock(CommandProcessingResult.class); + when(commandProcessingResult.getOfficeId()).thenReturn(1L); + when(datatableUtil.checkMainResourceExistsWithinScope(any(EntityTables.class), anyLong())).thenReturn(commandProcessingResult); + } + + private void setupCreateDatatableEntryMocks() { + List columnHeaders = new ArrayList<>(); + when(genericDataService.fillResultsetColumnHeaders(anyString())).thenReturn(columnHeaders); + when(jdbcTemplate.update(any(PreparedStatementCreator.class), any(KeyHolder.class))).thenReturn(1); - underTest.createNewDatatableEntry(DATATABLE_NAME, 1L, createJsonCommand("{}", 1L)); + GeneratedKeyHolder keyHolder = Mockito.mock(GeneratedKeyHolder.class); + when(keyHolder.getKeys()).thenReturn(Map.of("id", 1L)); + when(sqlGenerator.fetchPK(any())).thenReturn(1L); - ArgumentCaptor> businessEventArgumentCaptor = verifyBusinessEvents(1); - verifyDatatableBusinessEvent(businessEventArgumentCaptor, 0, 1L); + when(fromJsonHelper.extractDataMap(any(), anyString())).thenReturn(new HashMap<>()); } - @Test - public void businessEventUpdateDatatableEntryTest() { - List values = new ArrayList<>(); - values.add(1L); - List result = new ArrayList<>(); - result.add(ResultsetRowData.create(values)); - when(genericDataService.fillResultsetRowData(anyString(), any())).thenReturn(result); + private void setupUpdateDeleteDatatableMocks() { + List columnHeaders = new ArrayList<>(); + List rowValues = new ArrayList<>(); + rowValues.add(1L); - // ResultsetColumnHeaderData resultSet = Mockito.mock(ResultsetColumnHeaderData.class); - // when(searchUtil.getFiltered(columnHeaders, - // ResultsetColumnHeaderData::getIsColumnPrimaryKey)).thenReturn(resultSet); + List rows = new ArrayList<>(); + rows.add(ResultsetRowData.create(rowValues)); - underTest.updateDatatableEntryOneToMany(DATATABLE_NAME, 1L, 1L, createJsonCommand("{}", 1L)); + GenericResultsetData resultData = new GenericResultsetData(columnHeaders, rows); - ArgumentCaptor> businessEventArgumentCaptor = verifyBusinessEvents(1); - verifyDatatableBusinessEvent(businessEventArgumentCaptor, 0, 1L); + Mockito.doReturn(resultData).when(datatableUtil).retrieveDataTableGenericResultSet(any(EntityTables.class), eq(DATATABLE_NAME), + eq(APP_TABLE_ID), isNull(), eq(DATATABLE_ID)); + + when(genericDataService.fillResultsetRowData(anyString(), any())).thenReturn(rows); } - @Test - public void businessEventDeleteDatatableEntryTest() { + private void verifyBusinessEvent(Long expectedEntityId) { + ArgumentCaptor> eventCaptor = ArgumentCaptor.forClass(BusinessEvent.class); + verify(businessEventNotifierService, times(1)).notifyPostBusinessEvent(eventCaptor.capture()); - underTest.deleteDatatableEntry(DATATABLE_NAME, 1L, 1L, createJsonCommand("{}", 1L)); + BusinessEvent capturedEvent = eventCaptor.getValue(); + assertInstanceOf(DatatableEntryBusinessEvent.class, capturedEvent); + DatatableEntryBusinessEvent event = (DatatableEntryBusinessEvent) capturedEvent; - ArgumentCaptor> businessEventArgumentCaptor = verifyBusinessEvents(1); - verifyDatatableBusinessEvent(businessEventArgumentCaptor, 0, 1L); - } - - @NotNull - private ArgumentCaptor> verifyBusinessEvents(int expectedBusinessEvents) { - @SuppressWarnings("unchecked") - ArgumentCaptor> businessEventArgumentCaptor = ArgumentCaptor.forClass(BusinessEvent.class); - verify(businessEventNotifierService, times(expectedBusinessEvents)).notifyPostBusinessEvent(businessEventArgumentCaptor.capture()); - return businessEventArgumentCaptor; - } - - private void verifyDatatableBusinessEvent(ArgumentCaptor> businessEventArgumentCaptor, int index, Long entityId) { - assertTrue(businessEventArgumentCaptor.getAllValues().get(index) instanceof DatatableEntryBusinessEvent); - assertEquals(DATATABLE_NAME, ((DatatableEntryBusinessEvent) businessEventArgumentCaptor.getAllValues().get(index)) - .getDatatableEntryDetails().getDatatableName()); - assertEquals(entityId, ((DatatableEntryBusinessEvent) businessEventArgumentCaptor.getAllValues().get(index)).getAggregateRootId()); + assertEquals(DATATABLE_NAME, event.getDatatableEntryDetails().getDatatableName()); + assertEquals(expectedEntityId, event.getAggregateRootId()); } private JsonCommand createJsonCommand(final String jsonCommand, final Long resourceId) { diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableReadServiceImplTest.java similarity index 98% rename from fineract-provider/src/test/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImplTest.java rename to fineract-provider/src/test/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableReadServiceImplTest.java index 018626b3f..291544833 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImplTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableReadServiceImplTest.java @@ -54,7 +54,7 @@ import org.springframework.test.context.ContextConfiguration; @SpringBootTest @ContextConfiguration(classes = TestConfiguration.class) @SuppressFBWarnings(value = "RV_EXCEPTION_NOT_THROWN", justification = "False positive") -public class ReadWriteNonCoreDataServiceImplTest { +public class DatatableReadServiceImplTest { @Autowired private JdbcTemplate jdbcTemplate; @@ -69,7 +69,7 @@ public class ReadWriteNonCoreDataServiceImplTest { private DatabaseSpecificSQLGenerator sqlGenerator; @Autowired - private ReadWriteNonCoreDataService underTest; + private DatatableReadService underTest; @Test public void testSqlInjectionCaughtQueryDataTable() {