diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index 9c14256ff..08af2792f 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -718,19 +718,19 @@ public class CommandWrapperBuilder { return this; } - public CommandWrapperBuilder createDatatable(final String datatable, final Long apptableId, final Long datatableId) { + public CommandWrapperBuilder createDatatableEntry(final String datatable, final Long apptableId, final Long datatableId) { this.actionName = "CREATE"; commonDatatableSettings(datatable, apptableId, datatableId); return this; } - public CommandWrapperBuilder updateDatatable(final String datatable, final Long apptableId, final Long datatableId) { + public CommandWrapperBuilder updateDatatableEntry(final String datatable, final Long apptableId, final Long datatableId) { this.actionName = "UPDATE"; commonDatatableSettings(datatable, apptableId, datatableId); return this; } - public CommandWrapperBuilder deleteDatatable(final String datatable, final Long apptableId, final Long datatableId) { + public CommandWrapperBuilder deleteDatatableEntry(final String datatable, final Long apptableId, final Long datatableId) { this.actionName = "DELETE"; commonDatatableSettings(datatable, apptableId, datatableId); return this; diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java index 159e54c17..1056bde38 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java @@ -24,6 +24,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.time.temporal.ChronoUnit; @@ -78,6 +79,14 @@ public final class DateUtils { return LocalDateTime.now(ZoneId.systemDefault()).truncatedTo(ChronoUnit.SECONDS); } + public static LocalDateTime getAuditLocalDateTime() { + return LocalDateTime.now(ZoneId.of("UTC")); + } + + public static OffsetDateTime getAuditOffsetDateTime() { + return OffsetDateTime.now(ZoneOffset.UTC); + } + public static boolean isDateInTheFuture(final LocalDate localDate) { return localDate.isAfter(getBusinessLocalDate()); } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGenerator.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGenerator.java index 115af018c..4c6a23429 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGenerator.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGenerator.java @@ -248,4 +248,19 @@ public class DatabaseSpecificSQLGenerator { return orderBy + orders.stream().map(e -> String.join(" ", alias(escape(e.getProperty()), alias), e.getDirection().name())) .collect(Collectors.joining(", ")); } + + public String buildInsert(@NotNull String definition, Collection fields) { + if (fields == null || fields.isEmpty()) { + return ""; + } + return "INSERT INTO " + escape(definition) + '(' + fields.stream().map(this::escape).collect(Collectors.joining(", ")) + + ") VALUES (?" + ", ?".repeat(fields.size() - 1) + ')'; + } + + public String buildUpdate(@NotNull String definition, Collection fields) { + if (fields == null || fields.isEmpty()) { + return ""; + } + return "UPDATE " + escape(definition) + " SET " + fields.stream().map(e -> escape(e) + " = ?").collect(Collectors.joining(", ")); + } } 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 f7c0919ff..38ddae0d4 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 @@ -314,7 +314,7 @@ public class DatatablesApiResource { @Parameter(hidden = true) final String apiRequestBodyAsJson) { final CommandWrapper commandRequest = new CommandWrapperBuilder() // - .createDatatable(datatable, apptableId, null) // + .createDatatableEntry(datatable, apptableId, null) // .withJson(apiRequestBodyAsJson) // .build(); @@ -336,7 +336,7 @@ public class DatatablesApiResource { @Parameter(hidden = true) final String apiRequestBodyAsJson) { final CommandWrapper commandRequest = new CommandWrapperBuilder() // - .updateDatatable(datatable, apptableId, null) // + .updateDatatableEntry(datatable, apptableId, null) // .withJson(apiRequestBodyAsJson) // .build(); @@ -359,7 +359,7 @@ public class DatatablesApiResource { @Parameter(hidden = true) final String apiRequestBodyAsJson) { final CommandWrapper commandRequest = new CommandWrapperBuilder() // - .updateDatatable(datatable, apptableId, datatableId) // + .updateDatatableEntry(datatable, apptableId, datatableId) // .withJson(apiRequestBodyAsJson) // .build(); @@ -380,7 +380,7 @@ public class DatatablesApiResource { @PathParam("apptableId") @Parameter(description = "apptableId") final Long apptableId) { final CommandWrapper commandRequest = new CommandWrapperBuilder() // - .deleteDatatable(datatable, apptableId, null) // + .deleteDatatableEntry(datatable, apptableId, null) // .build(); final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); @@ -396,13 +396,11 @@ public class DatatablesApiResource { + "\n") @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = DatatablesApiResourceSwagger.DeleteDataTablesDatatableAppTableIdDatatableIdResponse.class))) }) - public String deleteDatatableEntries( - @PathParam("datatable") @Parameter(description = "datatable", example = "{}") final String datatable, + public String deleteDatatableEntry(@PathParam("datatable") @Parameter(description = "datatable", example = "{}") final String datatable, @PathParam("apptableId") @Parameter(description = "apptableId") final Long apptableId, @PathParam("datatableId") @Parameter(description = "datatableId") final Long datatableId) { - final CommandWrapper commandRequest = new CommandWrapperBuilder() // - .deleteDatatable(datatable, apptableId, datatableId) // + .deleteDatatableEntry(datatable, apptableId, datatableId) // .build(); final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); 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 6c916fbc3..ee89b87aa 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 @@ -21,7 +21,6 @@ package org.apache.fineract.infrastructure.dataqueries.handler; 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.springframework.stereotype.Service; @@ -40,18 +39,6 @@ public class DeleteOneToManyDatatableEntryCommandHandler implements NewCommandSo @Transactional @Override public CommandProcessingResult processCommand(final JsonCommand command) { - - final CommandProcessingResult commandProcessingResult = this.writePlatformService.deleteDatatableEntry(command.entityName(), - command.entityId(), command.subentityId()); - - return new CommandProcessingResultBuilder() // - .withCommandId(command.commandId()) // - .withEntityId(command.entityId()) // - .withOfficeId(commandProcessingResult.getOfficeId()) // - .withGroupId(commandProcessingResult.getGroupId()) // - .withClientId(commandProcessingResult.getClientId()) // - .withSavingsId(commandProcessingResult.getSavingsId()) // - .withLoanId(commandProcessingResult.getLoanId()) // - .build(); + return this.writePlatformService.deleteDatatableEntry(command.entityName(), 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 705dc4e16..ca67ea8fe 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 @@ -21,7 +21,6 @@ package org.apache.fineract.infrastructure.dataqueries.handler; 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.springframework.stereotype.Service; @@ -40,18 +39,6 @@ public class DeleteOneToOneDatatableEntryCommandHandler implements NewCommandSou @Transactional @Override public CommandProcessingResult processCommand(final JsonCommand command) { - - final CommandProcessingResult commandProcessingResult = this.writePlatformService.deleteDatatableEntries(command.entityName(), - command.entityId()); - - return new CommandProcessingResultBuilder() // - .withCommandId(command.commandId()) // - .withEntityId(command.entityId()) // - .withOfficeId(commandProcessingResult.getOfficeId()) // - .withGroupId(commandProcessingResult.getGroupId()) // - .withClientId(commandProcessingResult.getClientId()) // - .withSavingsId(commandProcessingResult.getSavingsId()) // - .withLoanId(commandProcessingResult.getLoanId()) // - .build(); + return this.writePlatformService.deleteDatatableEntries(command.entityName(), command.entityId(), command); } } 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 dc42431a4..e943d0677 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 @@ -272,5 +272,4 @@ public class EntityDatatableChecksWritePlatformServiceImpl implements EntityData throw new PlatformDataIntegrityException("error.msg.report.unknown.data.integrity.issue", "Unknown data integrity issue with resource: " + realCause.getMessage()); } - } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java index cef0a1f4f..685a9f013 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java @@ -68,9 +68,9 @@ public interface ReadWriteNonCoreDataService { CommandProcessingResult updateDatatableEntryOneToMany(String datatable, Long appTableId, Long datatableId, JsonCommand command); - CommandProcessingResult deleteDatatableEntries(String datatable, Long appTableId); + CommandProcessingResult deleteDatatableEntries(String datatable, Long appTableId, JsonCommand command); - CommandProcessingResult deleteDatatableEntry(String datatable, Long appTableId, Long datatableId); + CommandProcessingResult deleteDatatableEntry(String datatable, Long appTableId, Long datatableId, JsonCommand command); String getTableName(String Url); 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/ReadWriteNonCoreDataServiceImpl.java index fb8184c32..a5ccb9331 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/ReadWriteNonCoreDataServiceImpl.java @@ -20,9 +20,10 @@ 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.BIT; +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; import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_FIELD_INDEXED; @@ -42,8 +43,13 @@ import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiCon import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_PARAM_DROPCOLUMNS; import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_PARAM_MULTIROW; 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; @@ -54,18 +60,15 @@ import jakarta.persistence.PersistenceException; import jakarta.validation.constraints.NotNull; import java.lang.reflect.Type; import java.math.BigDecimal; -import java.sql.SQLException; -import java.sql.Statement; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Set; import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -77,11 +80,12 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; -import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException; import org.apache.fineract.infrastructure.core.serialization.DatatableCommandFromApiJsonDeserializer; 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; @@ -115,8 +119,6 @@ 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.KeyHolder; import org.springframework.jdbc.support.rowset.SqlRowSet; import org.springframework.orm.jpa.JpaSystemException; import org.springframework.transaction.annotation.Transactional; @@ -329,10 +331,6 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ return true; } - private void logAsErrorUnexpectedDataIntegrityException(final Exception dve) { - log.error("Error occurred.", dve); - } - @Transactional @Override public void registerDatatable(final String dataTableName, final String entityName, final String entitySubType) { @@ -390,9 +388,9 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ this.jdbcTemplate.update(permissionsSql); // add the registered table to the config if it is a ppi - if (this.isSurveyCategory(category)) { + if (category.equals(DataTableApiConstant.CATEGORY_PPI)) { this.namedParameterJdbcTemplate - .update("insert into c_configuration (name, value, enabled ) values( :dataTableName , '0',false)", paramMap); + .update("insert into c_configuration (name, value, enabled ) values( :dataTableName, '0', false)", paramMap); } } catch (final JpaSystemException | DataIntegrityViolationException dve) { @@ -443,28 +441,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ private Integer getCategory(final JsonCommand command) { Integer category = command.integerValueOfParameterNamedDefaultToNullIfZero(DataTableApiConstant.categoryParamName); - if (category == null) { - category = DataTableApiConstant.CATEGORY_DEFAULT; - } - return category; - } - - private boolean isSurveyCategory(final Integer category) { - return category.equals(DataTableApiConstant.CATEGORY_PPI); - } - - 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, (isMandatory ? "true" : "false")); - column.addProperty(API_FIELD_UNIQUE, (isUnique ? "true" : "false")); - column.addProperty(API_FIELD_INDEXED, (isIndexed ? "true" : "false")); - return column; + return category == null ? DataTableApiConstant.CATEGORY_DEFAULT : category; } @Override @@ -506,136 +483,6 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ this.jdbcTemplate.batchUpdate(sqlArray); // NOSONAR } - @Transactional - @Override - public CommandProcessingResult createNewDatatableEntry(final String dataTableName, final Long appTableId, final JsonCommand command) { - return createNewDatatableEntry(dataTableName, appTableId, command.json()); - } - - @Transactional - @Override - public CommandProcessingResult createNewDatatableEntry(final String dataTableName, final Long appTableId, final String json) { - try { - final EntityTables entityTable = queryForApplicationEntity(dataTableName); - CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(entityTable, appTableId); - - final List columnHeaders = this.genericDataService.fillResultsetColumnHeaders(dataTableName); - - final boolean multiRow = isMultirowDatatable(columnHeaders); - - final Type typeOfMap = new TypeToken>() {}.getType(); - final Map dataParams = this.fromJsonHelper.extractDataMap(typeOfMap, json); - - final String sql = getAddSql(columnHeaders, dataTableName, getFKField(entityTable), appTableId, dataParams); - - if (!multiRow) { - this.jdbcTemplate.update(sql); - commandProcessingResult = CommandProcessingResult.fromCommandProcessingResult(commandProcessingResult, appTableId); - } else { - final Long resourceId = addMultirowRecord(sql); - commandProcessingResult = CommandProcessingResult.fromCommandProcessingResult(commandProcessingResult, resourceId); - } - - return commandProcessingResult; // - - } catch (final SQLException e) { - throw new PlatformDataIntegrityException("error.msg.unknown.data.integrity.issue", - "Unknown data integrity issue with resource.", e); - } catch (final DataAccessException dve) { - final Throwable cause = dve.getCause(); - final Throwable realCause = dve.getMostSpecificCause(); - if (realCause.getMessage().contains("Duplicate entry") || cause.getMessage().contains("Duplicate entry")) { - throw new PlatformDataIntegrityException( - "error.msg.datatable.entry.duplicate", "An entry already exists for datatable `" + dataTableName - + "` and application table with identifier `" + appTableId + "`.", - API_PARAM_DATATABLE_NAME, dataTableName, appTableId, dve); - } else if (realCause.getMessage().contains("doesn't have a default value") - || cause.getMessage().contains("doesn't have a default value")) { - throw new PlatformDataIntegrityException( - "error.msg.datatable.no.value.provided.for.required.fields", "No values provided for the datatable `" - + dataTableName + "` and application table with identifier `" + appTableId + "`.", - API_PARAM_DATATABLE_NAME, dataTableName, appTableId, dve); - } - - logAsErrorUnexpectedDataIntegrityException(dve); - throw new PlatformDataIntegrityException("error.msg.unknown.data.integrity.issue", - "Unknown data integrity issue with resource.", dve); - } catch (final PersistenceException e) { - final Throwable cause = e.getCause(); - if (cause.getMessage().contains("Duplicate entry")) { - throw new PlatformDataIntegrityException( - "error.msg.datatable.entry.duplicate", "An entry already exists for datatable `" + dataTableName - + "` and application table with identifier `" + appTableId + "`.", - API_PARAM_DATATABLE_NAME, dataTableName, appTableId, e); - } else if (cause.getMessage().contains("doesn't have a default value")) { - throw new PlatformDataIntegrityException( - "error.msg.datatable.no.value.provided.for.required.fields", "No values provided for the datatable `" - + dataTableName + "` and application table with identifier `" + appTableId + "`.", - API_PARAM_DATATABLE_NAME, dataTableName, appTableId, e); - } - - logAsErrorUnexpectedDataIntegrityException(e); - throw new PlatformDataIntegrityException("error.msg.unknown.data.integrity.issue", - "Unknown data integrity issue with resource.", e); - } - } - - @Override - public CommandProcessingResult createPPIEntry(final String dataTableName, final Long appTableId, final JsonCommand command) { - try { - final EntityTables entityTable = queryForApplicationEntity(dataTableName); - final CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(entityTable, appTableId); - - final List columnHeaders = this.genericDataService.fillResultsetColumnHeaders(dataTableName); - - final Type typeOfMap = new TypeToken>() {}.getType(); - final Map dataParams = this.fromJsonHelper.extractDataMap(typeOfMap, command.json()); - - final String sql = getAddSqlWithScore(columnHeaders, dataTableName, getFKField(entityTable), appTableId, dataParams); - - this.jdbcTemplate.update(sql); - - return commandProcessingResult; // - - } catch (final DataAccessException dve) { - final Throwable cause = dve.getCause(); - final Throwable realCause = dve.getMostSpecificCause(); - if (realCause.getMessage().contains("Duplicate entry") || cause.getMessage().contains("Duplicate entry")) { - throw new PlatformDataIntegrityException( - "error.msg.datatable.entry.duplicate", "An entry already exists for datatable `" + dataTableName - + "` and application table with identifier `" + appTableId + "`.", - API_PARAM_DATATABLE_NAME, dataTableName, appTableId, dve); - } - - logAsErrorUnexpectedDataIntegrityException(dve); - throw new PlatformDataIntegrityException("error.msg.unknown.data.integrity.issue", - "Unknown data integrity issue with resource.", dve); - } catch (final PersistenceException dve) { - final Throwable cause = dve.getCause(); - if (cause.getMessage().contains("Duplicate entry")) { - throw new PlatformDataIntegrityException( - "error.msg.datatable.entry.duplicate", "An entry already exists for datatable `" + dataTableName - + "` and application table with identifier `" + appTableId + "`.", - API_PARAM_DATATABLE_NAME, dataTableName, appTableId, dve); - } - - logAsErrorUnexpectedDataIntegrityException(dve); - throw new PlatformDataIntegrityException("error.msg.unknown.data.integrity.issue", - "Unknown data integrity issue with resource.", dve); - } - } - - private String datatableColumnNameToCodeValueName(final String columnName, final String code) { - return code + "_cd_" + columnName; - } - - private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", - dataValidationErrors); - } - } - private void parseDatatableColumnObjectForCreate(final JsonObject column, StringBuilder sqlBuilder, final StringBuilder constrainBuilder, final String dataTableNameAlias, final Map codeMappings, final boolean isConstraintApproach) { @@ -722,8 +569,8 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ sqlBuilder.append(sqlGenerator.escape(fkColumnName)).append(" BIGINT NOT NULL, "); // Add Created At and Updated At - columns.add(addColumn(DataTableApiConstant.CREATEDAT_FIELD_NAME, DATETIME, false, null, false, false)); - columns.add(addColumn(DataTableApiConstant.UPDATEDAT_FIELD_NAME, DATETIME, false, null, false, false)); + columns.add(addColumn(CREATEDAT_FIELD_NAME, DATETIME, false, null, false, false)); + columns.add(addColumn(UPDATEDAT_FIELD_NAME, DATETIME, false, null, false, false)); final Map codeMappings = new HashMap<>(); final StringBuilder constrainBuilder = new StringBuilder(); @@ -786,13 +633,26 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } else if (realCause.getMessage().contains("Row") && realCause.getMessage().contains("large")) { baseDataValidator.reset().parameter("row").failWithCode("size.too.large"); } - - throwExceptionIfValidationWarningsExist(dataValidationErrors); + baseDataValidator.throwValidationErrors(); } 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); @@ -814,252 +674,6 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } } - private long addMultirowRecord(String sql) throws SQLException { - KeyHolder keyHolder = new GeneratedKeyHolder(); - int insertsCount = this.jdbcTemplate.update(c -> c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS), keyHolder); - if (insertsCount == 1) { - Number assignedKey; - if (keyHolder.getKeys().size() > 1) { - assignedKey = (Long) keyHolder.getKeys().get(TABLE_FIELD_ID); - } else { - assignedKey = keyHolder.getKey(); - } - if (assignedKey == null) { - throw new SQLException("Row id getting error."); - } - return assignedKey.longValue(); - } - throw new SQLException("Expected one inserted row."); - } - - private void parseDatatableColumnForUpdate(final JsonObject column, - final Map mapColumnNameDefinition, final String datatableName, StringBuilder renameBuilder, - StringBuilder changeBuilder, final StringBuilder constrainBuilder, final Map codeMappings, - final List removeMappings, final boolean isConstraintApproach) { - String oldName = column.has(API_FIELD_NAME) ? column.get(API_FIELD_NAME).getAsString() : null; - if (!mapColumnNameDefinition.containsKey(oldName)) { - throw new PlatformDataIntegrityException("error.msg.datatable.column.missing.update.parse", - "Column " + oldName + " does not exist.", oldName); - } - final String lengthStr = column.has(API_FIELD_LENGTH) ? column.get(API_FIELD_LENGTH).getAsString() : null; - Long length = StringUtils.isNotBlank(lengthStr) ? Long.parseLong(lengthStr) : null; - String newName = column.has(API_FIELD_NEWNAME) ? column.get(API_FIELD_NEWNAME).getAsString() : null; - final Boolean newMandatory = column.has(API_FIELD_MANDATORY) ? column.get(API_FIELD_MANDATORY).getAsBoolean() : null; - final String after = column.has(API_FIELD_AFTER) ? column.get(API_FIELD_AFTER).getAsString() : null; - final String code = column.has(API_FIELD_CODE) ? column.get(API_FIELD_CODE).getAsString() : null; - final String newCode = column.has(API_FIELD_NEWCODE) ? column.get(API_FIELD_NEWCODE).getAsString() : null; - final String dataTableNameAlias = datatableName.toLowerCase().replaceAll("\\s", "_"); - if (isConstraintApproach) { - if (StringUtils.isBlank(newName)) { - newName = oldName; - } - String fkName = "fk_" + dataTableNameAlias + "_" + oldName; - String newFkName = "fk_" + dataTableNameAlias + "_" + newName; - if (!StringUtils.equalsIgnoreCase(code, newCode) || !StringUtils.equalsIgnoreCase(oldName, newName)) { - if (StringUtils.equalsIgnoreCase(code, newCode)) { - final int codeId = getCodeIdForColumn(dataTableNameAlias, oldName); - if (codeId > 0) { - removeMappings.add(dataTableNameAlias + "_" + oldName); - constrainBuilder.append(", DROP CONSTRAINT ").append(sqlGenerator.escape(fkName)).append(" "); - codeMappings.put(dataTableNameAlias + "_" + newName, (long) codeId); - constrainBuilder.append(", ADD CONSTRAINT ").append(sqlGenerator.escape(newFkName)).append(" ") - .append("FOREIGN KEY (").append(sqlGenerator.escape(newName)).append(") ").append("REFERENCES ") - .append(sqlGenerator.escape(CODE_VALUES_TABLE)).append(" (").append(TABLE_FIELD_ID).append(")"); - } - - } else { - if (code != null) { - removeMappings.add(dataTableNameAlias + "_" + oldName); - if (newCode == null || !StringUtils.equalsIgnoreCase(oldName, newName)) { - constrainBuilder.append(", DROP CONSTRAINT ").append(sqlGenerator.escape(fkName)).append(" "); - } - } - if (newCode != null) { - codeMappings.put(dataTableNameAlias + "_" + newName, this.codeReadPlatformService.retriveCode(newCode).getId()); - if (code == null || !StringUtils.equalsIgnoreCase(oldName, newName)) { - constrainBuilder.append(", ADD CONSTRAINT ").append(sqlGenerator.escape(newFkName)).append(" ") - .append("FOREIGN KEY (").append(sqlGenerator.escape(newName)).append(") ").append("REFERENCES ") - .append(sqlGenerator.escape(CODE_VALUES_TABLE)).append(" (").append(TABLE_FIELD_ID).append(")"); - } - } - } - } - } else { - if (StringUtils.isNotBlank(code)) { - oldName = datatableColumnNameToCodeValueName(oldName, code); - if (StringUtils.isNotBlank(newCode)) { - newName = datatableColumnNameToCodeValueName(newName, newCode); - } else { - newName = datatableColumnNameToCodeValueName(newName, code); - } - } - } - DatabaseType dialect = databaseTypeResolver.databaseType(); - ResultsetColumnHeaderData columnHeader = mapColumnNameDefinition.get(oldName); - final JdbcJavaType type = columnHeader.getColumnType(); - boolean nameChanged = !StringUtils.isBlank(newName) && !newName.equals(oldName); - boolean lengthChanged = length != null && !length.equals(columnHeader.getColumnLength()) && type.hasPrecision(dialect); - boolean nullityChanged = newMandatory != null && newMandatory != columnHeader.isMandatory(); - boolean afterChanged = after != null && databaseTypeResolver.isMySQL(); - if (nameChanged || lengthChanged || nullityChanged || afterChanged) { - Integer precision = length == null ? null : length.intValue(); - Integer scale = null; - if (type.isDecimalType()) { - precision = 19; - scale = 6; - } - String colName = StringUtils.isBlank(newName) ? oldName : newName; - boolean mandatory = newMandatory == null ? columnHeader.isMandatory() : newMandatory; - if (databaseTypeResolver.isMySQL()) { - String modifySql = nameChanged ? ("CHANGE " + sqlGenerator.escape(oldName) + " " + sqlGenerator.escape(colName)) - : (" MODIFY " + sqlGenerator.escape(colName)); - changeBuilder.append(", ").append(modifySql).append(" ").append(type.formatSql(dialect, precision, scale)) - .append(mandatory ? " NOT NULL" : " DEFAULT NULL"); - if (after != null) { - changeBuilder.append(" AFTER ").append(sqlGenerator.escape(after)); - } - } else { - if (nameChanged) { - renameBuilder.append("ALTER TABLE ").append(sqlGenerator.escape(datatableName)).append(" RENAME COLUMN ") - .append(sqlGenerator.escape(oldName)).append(" TO ").append(sqlGenerator.escape(newName)).append("; "); - } - if (lengthChanged) { - changeBuilder.append(", ALTER ").append(sqlGenerator.escape(colName)).append(" type ") - .append(type.formatSql(dialect, precision, scale)); - } - if (nullityChanged) { - changeBuilder.append(", ALTER ").append(sqlGenerator.escape(colName)) - .append(mandatory ? " set not null" : " drop not null"); - } - } - } - } - - private int getCodeIdForColumn(final String dataTableNameAlias, final String name) { - final StringBuilder checkColumnCodeMapping = new StringBuilder(); - checkColumnCodeMapping.append("select ccm.code_id from x_table_column_code_mappings ccm where ccm.column_alias_name='") - .append(dataTableNameAlias).append("_").append(name).append("'"); - Integer codeId = 0; - try { - codeId = this.jdbcTemplate.queryForObject(checkColumnCodeMapping.toString(), Integer.class); - } catch (final EmptyResultDataAccessException e) { - log.warn("Error occurred.", e); - } - return ObjectUtils.defaultIfNull(codeId, 0); - } - - private void parseDatatableColumnForAdd(final JsonObject column, StringBuilder sqlBuilder, final String dataTableNameAlias, - final StringBuilder constrainBuilder, 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 after = column.has(API_FIELD_AFTER) ? column.get(API_FIELD_AFTER).getAsString() : null; - final String code = column.has(API_FIELD_CODE) ? column.get(API_FIELD_CODE).getAsString() : null; - - if (StringUtils.isNotBlank(code)) { - if (isConstraintApproach) { - String fkName = "fk_" + dataTableNameAlias + "_" + name; - codeMappings.put(dataTableNameAlias + "_" + name, this.codeReadPlatformService.retriveCode(code).getId()); - constrainBuilder.append(",ADD CONSTRAINT ").append(sqlGenerator.escape(fkName)).append(" ").append("FOREIGN KEY (") - .append(sqlGenerator.escape(name)).append(") ").append("REFERENCES ").append(sqlGenerator.escape(CODE_VALUES_TABLE)) - .append(" (").append(TABLE_FIELD_ID).append(")"); - } else { - name = datatableColumnNameToCodeValueName(name, code); - } - } - sqlBuilder.append(", ADD ").append(sqlGenerator.escape(name)).append(" ").append(mapApiTypeToDbType(type, length)); - - if (unique) { - String uniqueKeyName = datatableKeywordGenerator.generateUniqueKeyName(dataTableNameAlias, name); - constrainBuilder.append(",ADD CONSTRAINT ").append(sqlGenerator.escape(uniqueKeyName)).append(" ").append("UNIQUE (") - .append(sqlGenerator.escape(name)).append(")"); - } - - if (mandatory) { - sqlBuilder.append(" NOT NULL"); - } else { - sqlBuilder.append(" DEFAULT NULL"); - } - - if (after != null) { - sqlBuilder.append(" AFTER ").append(sqlGenerator.escape(after)); - } - } - - private void parseDatatableColumnForDrop(final JsonObject column, StringBuilder sqlBuilder, final String datatableName, - final StringBuilder constrainBuilder, final List codeMappings) { - final String datatableAlias = datatableName.toLowerCase().replaceAll("\\s", "_"); - final String name = column.has(API_FIELD_NAME) ? column.get(API_FIELD_NAME).getAsString() : null; - if (name == null) { - throw new GeneralPlatformDomainRuleException("error.msg.missing.datatable.column.name", - "Datatable column name to drop is missing."); - } - sqlBuilder.append(", DROP COLUMN ").append(sqlGenerator.escape(name)).append(" "); - - String fkName = "fk_" + datatableAlias + "_" + name; - String schemaSql = databaseTypeResolver.isMySQL() ? "i.TABLE_SCHEMA = SCHEMA()" - : "i.table_catalog = current_catalog AND i.table_schema = current_schema"; - String findFKSql = "SELECT count(*) FROM information_schema.TABLE_CONSTRAINTS i" + " WHERE i.CONSTRAINT_TYPE = 'FOREIGN KEY' AND " - + schemaSql + " AND i.TABLE_NAME = '" + datatableName + "' AND i.CONSTRAINT_NAME = '" + fkName + "' "; - - final int count = this.jdbcTemplate.queryForObject(findFKSql, Integer.class); - if (count > 0) { - codeMappings.add(datatableAlias + "_" + name); - constrainBuilder.append(", DROP FOREIGN KEY ").append(sqlGenerator.escape(fkName)).append(" "); - } - } - - private void registerColumnCodeMapping(final Map codeMappings) { - if (codeMappings != null && !codeMappings.isEmpty()) { - final String[] addSqlList = new String[codeMappings.size()]; - int i = 0; - for (final Map.Entry mapEntry : codeMappings.entrySet()) { - addSqlList[i++] = "insert into x_table_column_code_mappings (column_alias_name, code_id) values ('" + mapEntry.getKey() - + "'," + mapEntry.getValue() + ");"; - } - - this.jdbcTemplate.batchUpdate(addSqlList); - } - } - - private void deleteColumnCodeMapping(final List columnNames) { - if (columnNames != null && !columnNames.isEmpty()) { - final String[] deleteSqlList = new String[columnNames.size()]; - int i = 0; - for (final String columnName : columnNames) { - deleteSqlList[i++] = "DELETE FROM x_table_column_code_mappings WHERE column_alias_name='" + columnName + "';"; - } - - this.jdbcTemplate.batchUpdate(deleteSqlList); - } - } - - /** - * Update data table, set column value to empty string where current value is NULL. Run update SQL only if the - * "mandatory" property is set to true - * - * @param datatableName - * Name of data table - * @param column - * JSON encoded array of column properties - * @see MIFOSX-1145 - **/ - private void removeNullValuesFromStringColumn(final String datatableName, final JsonObject column, - final Map mapColumnNameDefinition) { - final boolean mandatory = column.has(API_FIELD_MANDATORY) && column.get(API_FIELD_MANDATORY).getAsBoolean(); - final String name = column.has(API_FIELD_NAME) ? column.get(API_FIELD_NAME).getAsString() : ""; - final JdbcJavaType type = mapColumnNameDefinition.containsKey(name) ? mapColumnNameDefinition.get(name).getColumnType() : null; - - if (type != null && mandatory && type.isStringType()) { - String sql = "UPDATE " + sqlGenerator.escape(datatableName) + " SET " + sqlGenerator.escape(name) + " = '' WHERE " - + sqlGenerator.escape(name) + " IS NULL"; - this.jdbcTemplate.update(sql); - } - } - @Transactional @Override public void updateDatatable(final String datatableName, final JsonCommand command) { @@ -1075,7 +689,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ final String entitySubType = this.fromJsonHelper.extractStringNamed(API_PARAM_SUBTYPE, element); validateDatatableName(datatableName); - int rowCount = getRowCount(datatableName); + int rowCount = getDatatableRowCount(datatableName); final List columnHeaderData = this.genericDataService.fillResultsetColumnHeaders(datatableName); final Map mapColumnNameDefinition = SearchUtil.mapHeadersToName(columnHeaderData); @@ -1240,8 +854,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } else if (realCause.getMessage().toLowerCase().contains("duplicate column")) { baseDataValidator.reset().parameter(API_FIELD_NAME).failWithCode("column.already.exists"); } - - throwExceptionIfValidationWarningsExist(dataValidationErrors); + baseDataValidator.throwValidationErrors(); } catch (final PersistenceException ee) { Throwable realCause = ExceptionUtils.getRootCause(ee.getCause()); final List dataValidationErrors = new ArrayList<>(); @@ -1256,8 +869,235 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } else if (realCause.getMessage().contains("Row") && realCause.getMessage().contains("large")) { baseDataValidator.reset().parameter("row").failWithCode("size.too.large"); } + baseDataValidator.throwValidationErrors(); + } + } - throwExceptionIfValidationWarningsExist(dataValidationErrors); + private void parseDatatableColumnForAdd(final JsonObject column, StringBuilder sqlBuilder, final String dataTableNameAlias, + final StringBuilder constrainBuilder, 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 after = column.has(API_FIELD_AFTER) ? column.get(API_FIELD_AFTER).getAsString() : null; + final String code = column.has(API_FIELD_CODE) ? column.get(API_FIELD_CODE).getAsString() : null; + + if (StringUtils.isNotBlank(code)) { + if (isConstraintApproach) { + String fkName = "fk_" + dataTableNameAlias + "_" + name; + codeMappings.put(dataTableNameAlias + "_" + name, this.codeReadPlatformService.retriveCode(code).getId()); + constrainBuilder.append(",ADD CONSTRAINT ").append(sqlGenerator.escape(fkName)).append(" ").append("FOREIGN KEY (") + .append(sqlGenerator.escape(name)).append(") ").append("REFERENCES ").append(sqlGenerator.escape(CODE_VALUES_TABLE)) + .append(" (").append(TABLE_FIELD_ID).append(")"); + } else { + name = datatableColumnNameToCodeValueName(name, code); + } + } + sqlBuilder.append(", ADD ").append(sqlGenerator.escape(name)).append(" ").append(mapApiTypeToDbType(type, length)); + + if (unique) { + String uniqueKeyName = datatableKeywordGenerator.generateUniqueKeyName(dataTableNameAlias, name); + constrainBuilder.append(",ADD CONSTRAINT ").append(sqlGenerator.escape(uniqueKeyName)).append(" ").append("UNIQUE (") + .append(sqlGenerator.escape(name)).append(")"); + } + + if (mandatory) { + sqlBuilder.append(" NOT NULL"); + } else { + sqlBuilder.append(" DEFAULT NULL"); + } + + if (after != null) { + sqlBuilder.append(" AFTER ").append(sqlGenerator.escape(after)); + } + } + + private void parseDatatableColumnForUpdate(final JsonObject column, + final Map mapColumnNameDefinition, final String datatableName, StringBuilder renameBuilder, + StringBuilder changeBuilder, final StringBuilder constrainBuilder, final Map codeMappings, + final List removeMappings, final boolean isConstraintApproach) { + String oldName = column.has(API_FIELD_NAME) ? column.get(API_FIELD_NAME).getAsString() : null; + if (!mapColumnNameDefinition.containsKey(oldName)) { + throw new PlatformDataIntegrityException("error.msg.datatable.column.missing.update.parse", + "Column " + oldName + " does not exist.", oldName); + } + final String lengthStr = column.has(API_FIELD_LENGTH) ? column.get(API_FIELD_LENGTH).getAsString() : null; + Long length = StringUtils.isNotBlank(lengthStr) ? Long.parseLong(lengthStr) : null; + String newName = column.has(API_FIELD_NEWNAME) ? column.get(API_FIELD_NEWNAME).getAsString() : null; + final Boolean newMandatory = column.has(API_FIELD_MANDATORY) ? column.get(API_FIELD_MANDATORY).getAsBoolean() : null; + final String after = column.has(API_FIELD_AFTER) ? column.get(API_FIELD_AFTER).getAsString() : null; + final String code = column.has(API_FIELD_CODE) ? column.get(API_FIELD_CODE).getAsString() : null; + final String newCode = column.has(API_FIELD_NEWCODE) ? column.get(API_FIELD_NEWCODE).getAsString() : null; + final String dataTableNameAlias = datatableName.toLowerCase().replaceAll("\\s", "_"); + if (isConstraintApproach) { + if (StringUtils.isBlank(newName)) { + newName = oldName; + } + String fkName = "fk_" + dataTableNameAlias + "_" + oldName; + String newFkName = "fk_" + dataTableNameAlias + "_" + newName; + if (!StringUtils.equalsIgnoreCase(code, newCode) || !StringUtils.equalsIgnoreCase(oldName, newName)) { + if (StringUtils.equalsIgnoreCase(code, newCode)) { + final int codeId = getCodeIdForColumn(dataTableNameAlias, oldName); + if (codeId > 0) { + removeMappings.add(dataTableNameAlias + "_" + oldName); + constrainBuilder.append(", DROP CONSTRAINT ").append(sqlGenerator.escape(fkName)).append(" "); + codeMappings.put(dataTableNameAlias + "_" + newName, (long) codeId); + constrainBuilder.append(", ADD CONSTRAINT ").append(sqlGenerator.escape(newFkName)).append(" ") + .append("FOREIGN KEY (").append(sqlGenerator.escape(newName)).append(") ").append("REFERENCES ") + .append(sqlGenerator.escape(CODE_VALUES_TABLE)).append(" (").append(TABLE_FIELD_ID).append(")"); + } + + } else { + if (code != null) { + removeMappings.add(dataTableNameAlias + "_" + oldName); + if (newCode == null || !StringUtils.equalsIgnoreCase(oldName, newName)) { + constrainBuilder.append(", DROP CONSTRAINT ").append(sqlGenerator.escape(fkName)).append(" "); + } + } + if (newCode != null) { + codeMappings.put(dataTableNameAlias + "_" + newName, this.codeReadPlatformService.retriveCode(newCode).getId()); + if (code == null || !StringUtils.equalsIgnoreCase(oldName, newName)) { + constrainBuilder.append(", ADD CONSTRAINT ").append(sqlGenerator.escape(newFkName)).append(" ") + .append("FOREIGN KEY (").append(sqlGenerator.escape(newName)).append(") ").append("REFERENCES ") + .append(sqlGenerator.escape(CODE_VALUES_TABLE)).append(" (").append(TABLE_FIELD_ID).append(")"); + } + } + } + } + } else { + if (StringUtils.isNotBlank(code)) { + oldName = datatableColumnNameToCodeValueName(oldName, code); + if (StringUtils.isNotBlank(newCode)) { + newName = datatableColumnNameToCodeValueName(newName, newCode); + } else { + newName = datatableColumnNameToCodeValueName(newName, code); + } + } + } + DatabaseType dialect = databaseTypeResolver.databaseType(); + ResultsetColumnHeaderData columnHeader = mapColumnNameDefinition.get(oldName); + final JdbcJavaType type = columnHeader.getColumnType(); + boolean nameChanged = !StringUtils.isBlank(newName) && !newName.equals(oldName); + boolean lengthChanged = length != null && !length.equals(columnHeader.getColumnLength()) && type.hasPrecision(dialect); + boolean nullityChanged = newMandatory != null && newMandatory != columnHeader.isMandatory(); + boolean afterChanged = after != null && databaseTypeResolver.isMySQL(); + if (nameChanged || lengthChanged || nullityChanged || afterChanged) { + Integer precision = length == null ? null : length.intValue(); + Integer scale = null; + if (type.isDecimalType()) { + precision = 19; + scale = 6; + } + String colName = StringUtils.isBlank(newName) ? oldName : newName; + boolean mandatory = newMandatory == null ? columnHeader.isMandatory() : newMandatory; + if (databaseTypeResolver.isMySQL()) { + String modifySql = nameChanged ? ("CHANGE " + sqlGenerator.escape(oldName) + " " + sqlGenerator.escape(colName)) + : (" MODIFY " + sqlGenerator.escape(colName)); + changeBuilder.append(", ").append(modifySql).append(" ").append(type.formatSql(dialect, precision, scale)) + .append(mandatory ? " NOT NULL" : " DEFAULT NULL"); + if (after != null) { + changeBuilder.append(" AFTER ").append(sqlGenerator.escape(after)); + } + } else { + if (nameChanged) { + renameBuilder.append("ALTER TABLE ").append(sqlGenerator.escape(datatableName)).append(" RENAME COLUMN ") + .append(sqlGenerator.escape(oldName)).append(" TO ").append(sqlGenerator.escape(newName)).append("; "); + } + if (lengthChanged) { + changeBuilder.append(", ALTER ").append(sqlGenerator.escape(colName)).append(" type ") + .append(type.formatSql(dialect, precision, scale)); + } + if (nullityChanged) { + changeBuilder.append(", ALTER ").append(sqlGenerator.escape(colName)) + .append(mandatory ? " set not null" : " drop not null"); + } + } + } + } + + private void parseDatatableColumnForDrop(final JsonObject column, StringBuilder sqlBuilder, final String datatableName, + final StringBuilder constrainBuilder, final List codeMappings) { + final String datatableAlias = datatableName.toLowerCase().replaceAll("\\s", "_"); + final String name = column.has(API_FIELD_NAME) ? column.get(API_FIELD_NAME).getAsString() : null; + if (name == null) { + throw new GeneralPlatformDomainRuleException("error.msg.missing.datatable.column.name", + "Datatable column name to drop is missing."); + } + sqlBuilder.append(", DROP COLUMN ").append(sqlGenerator.escape(name)).append(" "); + + String fkName = "fk_" + datatableAlias + "_" + name; + String schemaSql = databaseTypeResolver.isMySQL() ? "i.TABLE_SCHEMA = SCHEMA()" + : "i.table_catalog = current_catalog AND i.table_schema = current_schema"; + String findFKSql = "SELECT count(*) FROM information_schema.TABLE_CONSTRAINTS i" + " WHERE i.CONSTRAINT_TYPE = 'FOREIGN KEY' AND " + + schemaSql + " AND i.TABLE_NAME = '" + datatableName + "' AND i.CONSTRAINT_NAME = '" + fkName + "' "; + + final Integer count = this.jdbcTemplate.queryForObject(findFKSql, Integer.class); + if (count != null && count > 0) { + codeMappings.add(datatableAlias + "_" + name); + constrainBuilder.append(", DROP FOREIGN KEY ").append(sqlGenerator.escape(fkName)).append(" "); + } + } + + private void registerColumnCodeMapping(final Map codeMappings) { + if (codeMappings != null && !codeMappings.isEmpty()) { + final String[] addSqlList = new String[codeMappings.size()]; + int i = 0; + for (final Map.Entry mapEntry : codeMappings.entrySet()) { + addSqlList[i++] = "insert into x_table_column_code_mappings (column_alias_name, code_id) values ('" + mapEntry.getKey() + + "'," + mapEntry.getValue() + ");"; + } + + this.jdbcTemplate.batchUpdate(addSqlList); + } + } + + private void deleteColumnCodeMapping(final List columnNames) { + if (columnNames != null && !columnNames.isEmpty()) { + final String[] deleteSqlList = new String[columnNames.size()]; + int i = 0; + for (final String columnName : columnNames) { + deleteSqlList[i++] = "DELETE FROM x_table_column_code_mappings WHERE column_alias_name='" + columnName + "';"; + } + + this.jdbcTemplate.batchUpdate(deleteSqlList); + } + } + + private int getCodeIdForColumn(final String dataTableNameAlias, final String name) { + final StringBuilder checkColumnCodeMapping = new StringBuilder(); + checkColumnCodeMapping.append("select ccm.code_id from x_table_column_code_mappings ccm where ccm.column_alias_name='") + .append(dataTableNameAlias).append("_").append(name).append("'"); + Integer codeId = 0; + try { + codeId = this.jdbcTemplate.queryForObject(checkColumnCodeMapping.toString(), Integer.class); + } catch (final EmptyResultDataAccessException e) { + log.warn("Error occurred.", e); + } + return ObjectUtils.defaultIfNull(codeId, 0); + } + + /** + * Update data table, set column value to empty string where current value is NULL. Run update SQL only if the + * "mandatory" property is set to true + * + * @param datatableName + * Name of data table + * @param column + * JSON encoded array of column properties + * @see MIFOSX-1145 + **/ + private void removeNullValuesFromStringColumn(final String datatableName, final JsonObject column, + final Map mapColumnNameDefinition) { + final boolean mandatory = column.has(API_FIELD_MANDATORY) && column.get(API_FIELD_MANDATORY).getAsBoolean(); + final String name = column.has(API_FIELD_NAME) ? column.get(API_FIELD_NAME).getAsString() : ""; + final JdbcJavaType type = mapColumnNameDefinition.containsKey(name) ? mapColumnNameDefinition.get(name).getColumnType() : null; + + if (type != null && mandatory && type.isStringType()) { + String sql = "UPDATE " + sqlGenerator.escape(datatableName) + " SET " + sqlGenerator.escape(name) + " = '' WHERE " + + sqlGenerator.escape(name) + " IS NULL"; + this.jdbcTemplate.update(sql); } } @@ -1432,22 +1272,132 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ if (realCause.getMessage().contains("Unknown table")) { baseDataValidator.reset().parameter(API_PARAM_DATATABLE_NAME).failWithCode("does.not.exist"); } - - throwExceptionIfValidationWarningsExist(dataValidationErrors); + baseDataValidator.throwValidationErrors(); } } - private void assertDataTableEmpty(final String datatableName) { - final int rowCount = getRowCount(datatableName); - if (rowCount != 0) { - throw new GeneralPlatformDomainRuleException("error.msg.non.empty.datatable.cannot.be.deleted", - "Non-empty datatable cannot be deleted."); + @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); + + List columnHeaders = genericDataService.fillResultsetColumnHeaders(dataTableName); + Map headersByName = SearchUtil.mapHeadersToName(columnHeaders); + + final Type typeOfMap = new TypeToken>() {}.getType(); + final Map dataParams = fromJsonHelper.extractDataMap(typeOfMap, json); + + final String dateFormat = dataParams.get(API_PARAM_DATE_FORMAT); + // fall back to dateFormat to keep backward compatibility + final String dateTimeFormat = dataParams.getOrDefault(API_PARAM_DATETIME_FORMAT, dateFormat); + final String localeString = dataParams.get(API_PARAM_LOCALE); + Locale locale = localeString == null ? null : JsonParserHelper.localeFromString(localeString); + + ArrayList insertColumns = new ArrayList<>( + List.of(entityTable.getForeignKeyColumnNameOnDatatable(), CREATEDAT_FIELD_NAME, UPDATEDAT_FIELD_NAME)); + LocalDateTime auditDateTime = DateUtils.getAuditLocalDateTime(); + ArrayList params = new ArrayList<>(List.of(appTableId, auditDateTime, auditDateTime)); + for (String key : dataParams.keySet()) { + if (isTechnicalParam(key)) { + continue; + } + ResultsetColumnHeaderData columnHeader = SearchUtil.validateToJdbcColumn(key, headersByName, false); + if (!isUserInsertable(entityTable, columnHeader)) { + continue; + } + insertColumns.add(columnHeader.getColumnName()); + params.add(SearchUtil.parseJdbcColumnValue(columnHeader, dataParams.get(key), dateFormat, dateTimeFormat, locale, false, + sqlGenerator)); + } + final String sql = sqlGenerator.buildInsert(dataTableName, insertColumns); + if (addScore) { + List scoreIds = params.stream().filter(e -> e != null && !String.valueOf(e).isBlank()).toList(); + int scoreValue; + if (scoreIds.isEmpty()) { + scoreValue = 0; + } else { + StringBuilder scoreSql = new StringBuilder("SELECT SUM(code_score) FROM m_code_value WHERE m_code_value."); + ArrayList scoreParams = new ArrayList<>(); + SearchUtil.buildCondition("id", BIGINT, IN, scoreIds, scoreSql, scoreParams, null, sqlGenerator); + Integer score = jdbcTemplate.queryForObject(scoreSql.toString(), Integer.class, scoreParams.toArray(Object[]::new)); + scoreValue = score == null ? 0 : score; + } + insertColumns.add("score"); + params.add(scoreValue); + } + try { + int updated = jdbcTemplate.update(sql, params.toArray(Object[]::new)); + if (updated != 1) { + throw new PlatformDataIntegrityException("error.msg.invalid.insert", "Expected one inserted row."); + } + + Long resourceId = appTableId; + if (isMultirowDatatable(columnHeaders)) { + resourceId = jdbcTemplate.queryForObject(DatabaseSpecificSQLGenerator.SELECT_CLAUSE.formatted(sqlGenerator.lastInsertId()), + Long.class); + } + return CommandProcessingResult.fromCommandProcessingResult(commandProcessingResult, resourceId); + } catch (final DataAccessException dve) { + final Throwable cause = dve.getCause(); + final Throwable realCause = dve.getMostSpecificCause(); + if (realCause.getMessage().contains("Duplicate entry") || cause.getMessage().contains("Duplicate entry")) { + throw new PlatformDataIntegrityException( + "error.msg.datatable.entry.duplicate", "An entry already exists for datatable `" + dataTableName + + "` and application table with identifier `" + appTableId + "`.", + API_PARAM_DATATABLE_NAME, dataTableName, appTableId, dve); + } else if (realCause.getMessage().contains("doesn't have a default value") + || cause.getMessage().contains("doesn't have a default value")) { + throw new PlatformDataIntegrityException( + "error.msg.datatable.no.value.provided.for.required.fields", "No values provided for the datatable `" + + dataTableName + "` and application table with identifier `" + appTableId + "`.", + API_PARAM_DATATABLE_NAME, dataTableName, appTableId, dve); + } + + logAsErrorUnexpectedDataIntegrityException(dve); + throw new PlatformDataIntegrityException("error.msg.unknown.data.integrity.issue", + "Unknown data integrity issue with resource.", dve); + } catch (final PersistenceException e) { + final Throwable cause = e.getCause(); + if (cause.getMessage().contains("Duplicate entry")) { + throw new PlatformDataIntegrityException( + "error.msg.datatable.entry.duplicate", "An entry already exists for datatable `" + dataTableName + + "` and application table with identifier `" + appTableId + "`.", + API_PARAM_DATATABLE_NAME, dataTableName, appTableId, e); + } else if (cause.getMessage().contains("doesn't have a default value")) { + throw new PlatformDataIntegrityException( + "error.msg.datatable.no.value.provided.for.required.fields", "No values provided for the datatable `" + + dataTableName + "` and application table with identifier `" + appTableId + "`.", + API_PARAM_DATATABLE_NAME, dataTableName, appTableId, e); + } + + logAsErrorUnexpectedDataIntegrityException(e); + throw new PlatformDataIntegrityException("error.msg.unknown.data.integrity.issue", + "Unknown data integrity issue with resource.", e); } } - private int getRowCount(final String datatableName) { - final String sql = "select count(*) from " + sqlGenerator.escape(datatableName); - return this.jdbcTemplate.queryForObject(sql, Integer.class); // NOSONAR + private static boolean isUserInsertable(@NotNull EntityTables entityTable, @NotNull ResultsetColumnHeaderData columnHeader) { + String columnName = columnHeader.getColumnName(); + return !columnHeader.getIsColumnPrimaryKey() && !CREATEDAT_FIELD_NAME.equals(columnName) && !UPDATEDAT_FIELD_NAME.equals(columnName) + && !entityTable.getForeignKeyColumnNameOnDatatable().equals(columnName); } @Transactional @@ -1464,78 +1414,108 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ 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); - final CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(entityTable, appTableId); + CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(entityTable, appTableId); - final GenericResultsetData grs = retrieveDataTableGenericResultSetForUpdate(entityTable, dataTableName, appTableId, datatableId); - - if (grs.hasNoEntries()) { + final GenericResultsetData existingRows = retrieveDataTableGenericResultSet(entityTable, dataTableName, appTableId, null, + datatableId); + if (existingRows.hasNoEntries()) { throw new DatatableNotFoundException(dataTableName, appTableId); } - - if (grs.hasMoreThanOneEntry()) { + if (existingRows.hasMoreThanOneEntry()) { throw new PlatformDataIntegrityException("error.msg.attempting.multiple.update", "Application table: " + dataTableName + " Foreign key id: " + appTableId); } - final Type typeOfMap = new TypeToken>() {}.getType(); - final Map dataParams = this.fromJsonHelper.extractDataMap(typeOfMap, command.json()); - - String pkName = TABLE_FIELD_ID; // 1:M datatable - if (datatableId == null) { - pkName = getFKField(entityTable); - } // 1:1 datatable - - final Map changes = getAffectedAndChangedColumns(grs, dataParams, pkName); - - if (!changes.isEmpty()) { - Long pkValue = appTableId; - if (datatableId != null) { - pkValue = datatableId; - } - final String sql = getUpdateSql(grs.getColumnHeaders(), dataTableName, pkName, pkValue, changes); - log.debug("Update sql: {}", sql); - if (StringUtils.isNotBlank(sql)) { - this.jdbcTemplate.update(sql); - } else { - log.debug("No Changes"); - } + List columnHeaders = existingRows.getColumnHeaders(); + if (isMultirowDatatable(columnHeaders) && datatableId == null) { + throw new PlatformDataIntegrityException("error.msg.attempting.multiple.update", + "Application table: " + dataTableName + " Foreign key id: " + appTableId); } + Map headersByName = SearchUtil.mapHeadersToName(columnHeaders); + final List existingValues = existingRows.getData().get(0).getRow(); + HashMap valuesByHeader = columnHeaders.stream().collect(HashMap::new, + (map, e) -> map.put(e, existingValues.get(map.size())), (map, map2) -> {}); + final Type typeOfMap = new TypeToken>() {}.getType(); + final Map dataParams = fromJsonHelper.extractDataMap(typeOfMap, command.json()); + + final String dateFormat = dataParams.get(API_PARAM_DATE_FORMAT); + // fall back to dateFormat to keep backward compatibility + final String dateTimeFormat = dataParams.getOrDefault(API_PARAM_DATETIME_FORMAT, dateFormat); + final String localeString = dataParams.get(API_PARAM_LOCALE); + Locale locale = localeString == null ? null : JsonParserHelper.localeFromString(localeString); + + DatabaseType dialect = sqlGenerator.getDialect(); + List updateColumns = new ArrayList<>(List.of(UPDATEDAT_FIELD_NAME)); + List params = new ArrayList<>(List.of(DateUtils.getAuditLocalDateTime())); + final HashMap changes = new HashMap<>(); + for (String key : dataParams.keySet()) { + if (isTechnicalParam(key)) { + continue; + } + ResultsetColumnHeaderData columnHeader = SearchUtil.validateToJdbcColumn(key, headersByName, false); + if (!isUserUpdatable(entityTable, columnHeader)) { + continue; + } + String columnName = columnHeader.getColumnName(); + Object existingValue = valuesByHeader.get(columnHeader); + Object columnValue = SearchUtil.parseColumnValue(columnHeader, dataParams.get(key), dateFormat, dateTimeFormat, locale, false, + sqlGenerator); + if ((columnHeader.getColumnType().isDecimalType() && MathUtil.isEqualTo((BigDecimal) existingValue, (BigDecimal) columnValue)) + || (existingValue == null ? columnValue == null : existingValue.equals(columnValue))) { + log.debug("Ignore change on update {}:{}", dataTableName, columnName); + continue; + } + updateColumns.add(columnName); + params.add(columnHeader.getColumnType().toJdbcValue(dialect, columnValue, false)); + changes.put(columnName, columnValue); + } + Long primaryKey = datatableId == null ? appTableId : datatableId; + if (!updateColumns.isEmpty()) { + ResultsetColumnHeaderData pkColumn = SearchUtil.getFiltered(columnHeaders, ResultsetColumnHeaderData::getIsColumnPrimaryKey); + params.add(primaryKey); + final String sql = sqlGenerator.buildUpdate(dataTableName, updateColumns) + " WHERE " + pkColumn.getColumnName() + " = ?"; + int updated = jdbcTemplate.update(sql, params.toArray(Object[]::new)); + if (updated != 1) { + throw new PlatformDataIntegrityException("error.msg.invalid.update", "Expected one updated row."); + } + } else { + log.debug("No change on update {}", dataTableName); + } return new CommandProcessingResultBuilder().withCommandId(command.commandId()) // - .withEntityId(datatableId != null ? command.subentityId() : command.entityId()) // + .withEntityId(primaryKey) // .withOfficeId(commandProcessingResult.getOfficeId()) // .withGroupId(commandProcessingResult.getGroupId()) // .withClientId(commandProcessingResult.getClientId()) // .withSavingsId(commandProcessingResult.getSavingsId()) // .withLoanId(commandProcessingResult.getLoanId()) // + .withTransactionId(commandProcessingResult.getTransactionId()) // .with(changes).build(); } - @Transactional - @Override - public CommandProcessingResult deleteDatatableEntries(final String dataTableName, final Long appTableId) { - validateDatatableName(dataTableName); - if (isDatatableAttachedToEntityDatatableCheck(dataTableName)) { - throw new DatatableEntryRequiredException(dataTableName, appTableId); - } - final EntityTables entityTable = queryForApplicationEntity(dataTableName); - final CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(entityTable, appTableId); - final String deleteOneToOneEntrySql = getDeleteEntriesSql(dataTableName, getFKField(entityTable), appTableId); - - final int rowsDeleted = this.jdbcTemplate.update(deleteOneToOneEntrySql); - if (rowsDeleted < 1) { - throw new DatatableNotFoundException(dataTableName, appTableId); - } - - return commandProcessingResult; + private static boolean isUserUpdatable(@NotNull EntityTables entityTable, @NotNull ResultsetColumnHeaderData columnHeader) { + return isUserInsertable(entityTable, columnHeader); } @Transactional @Override - public CommandProcessingResult deleteDatatableEntry(final String dataTableName, final Long appTableId, final Long datatableId) { + 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); if (isDatatableAttachedToEntityDatatableCheck(dataTableName)) { throw new DatatableEntryRequiredException(dataTableName, appTableId); @@ -1543,10 +1523,29 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ final EntityTables entityTable = queryForApplicationEntity(dataTableName); final CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(entityTable, appTableId); - final String sql = getDeleteEntrySql(dataTableName, datatableId); + String whereColumn; + Long whereValue; + if (datatableId == null) { + whereColumn = getFKField(entityTable); + whereValue = appTableId; + } else { + whereColumn = TABLE_FIELD_ID; + whereValue = datatableId; + } + String sql = "DELETE FROM " + sqlGenerator.escape(dataTableName) + " WHERE " + sqlGenerator.escape(whereColumn) + " = " + + whereValue; this.jdbcTemplate.update(sql); - return commandProcessingResult; + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(whereValue) // + .withOfficeId(commandProcessingResult.getOfficeId()) // + .withGroupId(commandProcessingResult.getGroupId()) // + .withClientId(commandProcessingResult.getClientId()) // + .withSavingsId(commandProcessingResult.getSavingsId()) // + .withLoanId(commandProcessingResult.getLoanId()) // + .withTransactionId(commandProcessingResult.getTransactionId()) // + .build(); } @Override @@ -1555,7 +1554,11 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ 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); @@ -1563,38 +1566,15 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ SQLInjectionValidator.validateSQLInput(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) + // 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 GenericResultsetData retrieveDataTableGenericResultSetForUpdate(final EntityTables entityTable, final String dataTableName, - final Long appTableId, final Long id) { - final List columnHeaders = this.genericDataService.fillResultsetColumnHeaders(dataTableName); - - final boolean multiRow = isMultirowDatatable(columnHeaders); - - String whereClause = getFKField(entityTable) + " = " + appTableId; - SQLInjectionValidator.validateSQLInput(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; - } - final List result = genericDataService.fillResultsetRowData(sql, columnHeaders); return new GenericResultsetData(columnHeaders, result); } @@ -1607,13 +1587,13 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ if (!rs.next()) { throw new DatatableNotFoundException(entityTable, appTableId); } - - final Long officeId = getLongSqlRowSet(rs, "officeId"); - final Long groupId = getLongSqlRowSet(rs, "groupId"); - final Long clientId = getLongSqlRowSet(rs, "clientId"); - final Long savingsId = getLongSqlRowSet(rs, "savingsId"); - final Long LoanId = getLongSqlRowSet(rs, "loanId"); - final Long entityId = getLongSqlRowSet(rs, "entityId"); + final Long officeId = rs.getLong("officeId"); + final Long groupId = rs.getLong("groupId"); + final Long clientId = rs.getLong("clientId"); + final Long savingsId = rs.getLong("savingsId"); + final Long loanId = rs.getLong("loanId"); + final Long transactionId = rs.getLong("transactionId"); + final Long entityId = rs.getLong("entityId"); if (rs.next()) { throw new DatatableSystemErrorException("System Error: More than one row returned from data scoping query"); @@ -1624,59 +1604,48 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ .withGroupId(groupId) // .withClientId(clientId) // .withSavingsId(savingsId) // - .withLoanId(LoanId).withEntityId(entityId)// + .withLoanId(loanId).withTransactionId(String.valueOf(transactionId)).withEntityId(entityId)// .build(); } - private Long getLongSqlRowSet(final SqlRowSet rs, final String column) { - Long val = rs.getLong(column); - if (val == 0) { - val = null; - } - return val; - } - 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 - */ - + // 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 thru 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) - */ + // 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 entityId from m_loan l " + 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 entityId from m_loan l " + + "(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 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 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 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 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 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 entityId from m_client c " + "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 entityId from m_group g " + "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 entityId from m_office o " - + " where o.hierarchy like '" + officeHierarchy + "%'" + " and o.id = " + appTableId; + "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, p.id as entityId from " + "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"); @@ -1710,281 +1679,6 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ return resolveEntity(applicationTableName); } - private String getFKField(EntityTables entityTable) { - return entityTable.getForeignKeyColumnNameOnDatatable(); - } - - private String getAddSql(final List columnHeaders, final String datatable, final String fkName, - final Long appTableId, final Map queryParams) { - final Map affectedColumns = getAffectedColumns(columnHeaders, queryParams, fkName); - - String pValueWrite = ""; - String addSql = ""; - final String singleQuote = "'"; - - String insertColumns = ""; - String selectColumns = ""; - String columnName = ""; - String pValue = null; - for (final ResultsetColumnHeaderData pColumnHeader : columnHeaders) { - final String key = pColumnHeader.getColumnName(); - if (affectedColumns.containsKey(key)) { - pValue = String.valueOf(affectedColumns.get(key)); - if (StringUtils.isEmpty(pValue) || "null".equalsIgnoreCase(pValue)) { - pValueWrite = "null"; - } else { - if (pColumnHeader.getColumnType() == BIT) { - if (databaseTypeResolver.isMySQL()) { - pValueWrite = BooleanUtils.toString(BooleanUtils.toBooleanObject(pValue), "1", "0", "null"); - } else if (databaseTypeResolver.isPostgreSQL()) { - pValueWrite = BooleanUtils.toString(BooleanUtils.toBooleanObject(pValue), "B'1'", "B'0'", "null"); - } else { - throw new IllegalStateException("Current database is not supported"); - } - } else { - pValueWrite = singleQuote + this.genericDataService.replace(pValue, singleQuote, singleQuote + singleQuote) - + singleQuote; - } - } - columnName = sqlGenerator.escape(key); - insertColumns += ", " + columnName; - selectColumns += "," + pValueWrite + " as " + columnName; - } else { - if (key.equalsIgnoreCase(DataTableApiConstant.CREATEDAT_FIELD_NAME) - || key.equalsIgnoreCase(DataTableApiConstant.UPDATEDAT_FIELD_NAME)) { - columnName = sqlGenerator.escape(key); - insertColumns += ", " + columnName; - selectColumns += "," + sqlGenerator.currentTenantDateTime() + " as " + columnName; - } - } - } - addSql = "insert into " + sqlGenerator.escape(datatable) + " (" + sqlGenerator.escape(fkName) + " " + insertColumns + ")" - + " select " + appTableId + " as id" + selectColumns; - - log.debug("{}", addSql); - return addSql; - } - - /** - * This method is used special for ppi cases Where the score need to be computed - * - * @param columnHeaders - * @param datatable - * @param fkName - * @param appTableId - * @param queryParams - * @return - */ - public String getAddSqlWithScore(final List columnHeaders, final String datatable, final String fkName, - final Long appTableId, final Map queryParams) { - final Map affectedColumns = getAffectedColumns(columnHeaders, queryParams, fkName); - - String pValueWrite = ""; - String scoresId = " "; - final String singleQuote = "'"; - - String insertColumns = ""; - String selectColumns = ""; - String columnName = ""; - String pValue = null; - for (final String key : affectedColumns.keySet()) { - pValue = String.valueOf(affectedColumns.get(key)); - - if (StringUtils.isEmpty(pValue) || "null".equalsIgnoreCase(pValue)) { - pValueWrite = "null"; - } else { - pValueWrite = singleQuote + this.genericDataService.replace(pValue, singleQuote, singleQuote + singleQuote) + singleQuote; - - scoresId += pValueWrite + " ,"; - - } - columnName = sqlGenerator.escape(key); - insertColumns += ", " + columnName; - selectColumns += "," + pValueWrite + " as " + columnName; - } - - scoresId = scoresId.replaceAll(" ,$", ""); - - String vaddSql = "insert into " + sqlGenerator.escape(datatable) + " (" + sqlGenerator.escape(fkName) + " " + insertColumns - + ", score )" + " select " + appTableId + " as id" + selectColumns - + " , ( SELECT SUM( code_score ) FROM m_code_value WHERE m_code_value.id IN (" + scoresId + " ) ) as score"; - - log.debug("{}", vaddSql); - return vaddSql; - } - - private String getUpdateSql(List columnHeaders, final String datatable, final String keyFieldName, - final Long keyFieldValue, final Map changedColumns) { - // just updating fields that have changed since pre-update read - though - // its possible these values are different from the page the user was - // looking at and even different from the current db values (if some - // other update got in quick) - would need a version field for - // completeness but its okay to take this risk with additional fields - // data - - if (changedColumns.size() == 0) { - return null; - } - - String pValue = null; - String pValueWrite = ""; - final String singleQuote = "'"; - boolean firstColumn = true; - String sql = "update " + sqlGenerator.escape(datatable) + " "; - for (final ResultsetColumnHeaderData pColumnHeader : columnHeaders) { - final String key = pColumnHeader.getColumnName(); - if (changedColumns.containsKey(key)) { - if (firstColumn) { - sql += " set "; - firstColumn = false; - } else { - sql += ", "; - } - - pValue = String.valueOf(changedColumns.get(key)); - if (StringUtils.isEmpty(pValue) || "null".equalsIgnoreCase(pValue)) { - pValueWrite = "null"; - } else { - if (pColumnHeader.getColumnType() == BIT) { - if (databaseTypeResolver.isMySQL()) { - pValueWrite = BooleanUtils.toString(BooleanUtils.toBooleanObject(pValue), "1", "0", "null"); - } else if (databaseTypeResolver.isPostgreSQL()) { - pValueWrite = BooleanUtils.toString(BooleanUtils.toBooleanObject(pValue), "B'1'", "B'0'", "null"); - } else { - throw new IllegalStateException("Current database is not supported"); - } - } else { - pValueWrite = singleQuote + this.genericDataService.replace(pValue, singleQuote, singleQuote + singleQuote) - + singleQuote; - } - } - sql += sqlGenerator.escape(key) + " = " + pValueWrite; - } else { - if (key.equalsIgnoreCase(DataTableApiConstant.UPDATEDAT_FIELD_NAME)) { - if (firstColumn) { - sql += " set "; - firstColumn = false; - } else { - sql += ", "; - } - sql += sqlGenerator.escape(key) + " = " + sqlGenerator.currentTenantDateTime(); - } - } - } - - sql += " where " + keyFieldName + " = " + keyFieldValue; - return sql; - } - - private Map getAffectedAndChangedColumns(final GenericResultsetData grs, final Map queryParams, - final String fkName) { - final Map affectedColumns = getAffectedColumns(grs.getColumnHeaders(), queryParams, fkName); - final Map affectedAndChangedColumns = new HashMap<>(); - - for (final String key : affectedColumns.keySet()) { - final Object columnValue = affectedColumns.get(key); - if (columnChanged(key, columnValue, grs)) { - affectedAndChangedColumns.put(key, columnValue); - } - } - return affectedAndChangedColumns; - } - - private boolean columnChanged(final String key, final Object value, final GenericResultsetData grs) { - final List columnValues = grs.getData().get(0).getRow(); - - for (int i = 0; i < grs.getColumnHeaders().size(); i++) { - if (key.equals(grs.getColumnHeaders().get(i).getColumnName())) { - return notTheSame(columnValues.get(i), value); - } - } - throw new PlatformDataIntegrityException("error.msg.invalid.columnName", "Parameter Column Name: " + key + " not found"); - } - - public Map getAffectedColumns(final List columnHeaders, - final Map queryParams, final String keyFieldName) { - - final String dateFormat = queryParams.get("dateFormat"); - Locale clientApplicationLocale = null; - final String localeQueryParam = queryParams.get("locale"); - if (!StringUtils.isBlank(localeQueryParam)) { - clientApplicationLocale = new Locale(queryParams.get("locale")); - } - - final String underscore = "_"; - final String space = " "; - String pValue = null; - Object validatedValue = null; - String queryParamColumnUnderscored; - String columnHeaderUnderscored; - boolean notFound; - - final Map affectedColumns = new HashMap<>(); - final Set keys = queryParams.keySet(); - for (final String key : keys) { - // ignores id and foreign key fields - // also ignores locale and dateformat fields that are used for - // validating numeric and date data - if (!(key.equalsIgnoreCase(TABLE_FIELD_ID) || key.equalsIgnoreCase(keyFieldName) || key.equals("locale") - || key.equals("dateFormat") || key.equals(DataTableApiConstant.CREATEDAT_FIELD_NAME) - || key.equals(DataTableApiConstant.UPDATEDAT_FIELD_NAME))) { - notFound = true; - // matches incoming fields with and without underscores (spaces - // and underscores considered the same) - queryParamColumnUnderscored = this.genericDataService.replace(key, space, underscore); - for (final ResultsetColumnHeaderData columnHeader : columnHeaders) { - if (notFound) { - columnHeaderUnderscored = this.genericDataService.replace(columnHeader.getColumnName(), space, underscore); - if (queryParamColumnUnderscored.equalsIgnoreCase(columnHeaderUnderscored)) { - pValue = queryParams.get(key); - validatedValue = SearchUtil.parseColumnValue(columnHeader, pValue, dateFormat, dateFormat, - clientApplicationLocale, true, sqlGenerator); - affectedColumns.put(columnHeader.getColumnName(), validatedValue); - notFound = false; - } - } - - } - if (notFound) { - throw new PlatformDataIntegrityException("error.msg.column.not.found", "Column: " + key + " Not Found"); - } - } - } - return affectedColumns; - } - - private String getDeleteEntriesSql(final String datatable, final String FKField, final Long appTableId) { - return "delete from " + sqlGenerator.escape(datatable) + " where " + sqlGenerator.escape(FKField) + " = " + appTableId; - } - - private String getDeleteEntrySql(final String datatable, final Long datatableId) { - return "delete from " + sqlGenerator.escape(datatable) + " where " + TABLE_FIELD_ID + " = " + datatableId; - } - - private boolean isMultirowDatatable(final List columnHeaders) { - boolean multiRow = false; - for (ResultsetColumnHeaderData column : columnHeaders) { - if (column.isNamed(TABLE_FIELD_ID)) { - multiRow = true; - break; - } - } - return multiRow; - } - - private boolean notTheSame(final Object currValue, final Object pValue) { - if (currValue == null && pValue == null) { - return false; - } else if (currValue == null || pValue == null) { - return true; - } - // Equals would fail if the scale is not the same - if (currValue instanceof BigDecimal && pValue instanceof BigDecimal) { - return !MathUtil.isEqualTo((BigDecimal) currValue, (BigDecimal) pValue); - } - return !currValue.equals(pValue); - } - @Override public Long countDatatableEntries(final String datatableName, final Long appTableId, String foreignKeyColumn) { final String sqlString = "SELECT COUNT(" + sqlGenerator.escape(foreignKeyColumn) + ") FROM " + sqlGenerator.escape(datatableName) @@ -1992,6 +1686,8 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ return this.jdbcTemplate.queryForObject(sqlString, Long.class); // NOSONAR } + // --- Validation --- + public 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" @@ -2000,25 +1696,6 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ return count != null && count > 0; } - // --- DbUtils --- - - @NotNull - private String mapApiTypeToDbType(String apiType, Integer length) { - if (StringUtils.isEmpty(apiType)) { - return ""; - } - JdbcJavaType jdbcType = DatatableCommandFromApiJsonDeserializer.mapApiTypeToJdbcType(apiType); - DatabaseType dialect = databaseTypeResolver.databaseType(); - if (jdbcType.isDecimalType()) { - return jdbcType.formatSql(dialect, 19, 6); // TODO: parameter length is not used - } else if (apiType.equalsIgnoreCase(API_FIELD_TYPE_DROPDOWN)) { - return jdbcType.formatSql(dialect, 11); // TODO: parameter length is not used - } - return jdbcType.formatSql(dialect, length); - } - - // --- Validation --- - private EntityTables resolveEntity(final String entityName) { EntityTables entityTable = EntityTables.fromEntityName(entityName); if (entityTable == null) { @@ -2060,4 +1737,57 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ datatableName); } } + + private void assertDataTableEmpty(final String datatableName) { + final int rowCount = getDatatableRowCount(datatableName); + if (rowCount != 0) { + throw new GeneralPlatformDomainRuleException("error.msg.non.empty.datatable.cannot.be.deleted", + "Non-empty datatable cannot be deleted."); + } + } + + // --- DbUtils --- + + @NotNull + private String mapApiTypeToDbType(String apiType, Integer length) { + if (StringUtils.isEmpty(apiType)) { + return ""; + } + JdbcJavaType jdbcType = DatatableCommandFromApiJsonDeserializer.mapApiTypeToJdbcType(apiType); + DatabaseType dialect = databaseTypeResolver.databaseType(); + if (jdbcType.isDecimalType()) { + return jdbcType.formatSql(dialect, 19, 6); // TODO: parameter length is not used + } else if (apiType.equalsIgnoreCase(API_FIELD_TYPE_DROPDOWN)) { + return jdbcType.formatSql(dialect, 11); // TODO: parameter length is not used + } + return jdbcType.formatSql(dialect, length); + } + + private int getDatatableRowCount(final String datatableName) { + final String sql = "select count(*) from " + sqlGenerator.escape(datatableName); + Integer count = this.jdbcTemplate.queryForObject(sql, Integer.class); + 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; + } + + private void logAsErrorUnexpectedDataIntegrityException(final Exception dve) { + log.error("Error occurred.", dve); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/survey/api/SurveyApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/survey/api/SurveyApiResource.java index b9fe1ec85..910f720e7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/survey/api/SurveyApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/survey/api/SurveyApiResource.java @@ -174,7 +174,7 @@ public class SurveyApiResource { @PathParam("fulfilledId") final Long fulfilledId) { final CommandWrapper commandRequest = new CommandWrapperBuilder() // - .deleteDatatable(surveyName, clientId, fulfilledId) // + .deleteDatatableEntry(surveyName, clientId, fulfilledId) // .build(); final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/SearchConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/SearchConstants.java index dbe099447..ed6a8106c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/SearchConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/SearchConstants.java @@ -31,6 +31,9 @@ public final class SearchConstants { public static final String API_PARAM_VALUES = "values"; public static final String API_PARAM_TABLE = "table"; public static final String API_PARAM_QUERY = "query"; + public static final String API_PARAM_DATE_FORMAT = "dateFormat"; + public static final String API_PARAM_DATETIME_FORMAT = "dateTimeFormat"; + public static final String API_PARAM_LOCALE = "locale"; private SearchConstants() {} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/service/SearchUtil.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/service/SearchUtil.java index 880613f0d..d8dcbba8e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/service/SearchUtil.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/service/SearchUtil.java @@ -86,8 +86,7 @@ public final class SearchUtil { @NotNull List resultColumns, @NotNull List results) { JsonObject json = new JsonObject(); for (int i = 0; i < selectColumns.size(); i++) { - String sCol = selectColumns.get(i).replaceAll("\"", ""); - Object rowValue = rowSet.getObject(sCol); + Object rowValue = rowSet.getObject(selectColumns.get(i)); if (rowValue != null) { String rCol = resultColumns.get(i); if (rowValue instanceof Character) { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionDatatableIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionDatatableIntegrationTest.java index bec3c9a75..0f4f666d5 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionDatatableIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionDatatableIntegrationTest.java @@ -178,7 +178,8 @@ public class SavingsAccountTransactionDatatableIntegrationTest { assertNotNull(response); - String datatableId = response.getResourceIdentifier(); + String createdName = response.getResourceIdentifier(); + assertEquals(datatableName, createdName); // add entries final HashMap datatableEntryMap = new HashMap<>(); @@ -190,13 +191,14 @@ public class SavingsAccountTransactionDatatableIntegrationTest { final boolean genericResultSet = true; - HashMap datatableEntryResponseFirst = this.datatableHelper.createDatatableEntry(datatableId, transactionId, + HashMap datatableEntryResponseFirst = this.datatableHelper.createDatatableEntry(datatableName, transactionId, genericResultSet, datatabelEntryRequestJsonString); - assertNotNull(datatableEntryResponseFirst.get("resourceId")); + Integer datatableId = (Integer) datatableEntryResponseFirst.get("resourceId"); + assertNotNull(datatableId); // Read the Datatable entry generated with genericResultSet - HashMap items = this.datatableHelper.readDatatableEntry(datatableId, transactionId, genericResultSet, null, ""); + HashMap items = this.datatableHelper.readDatatableEntry(datatableName, transactionId, genericResultSet, null, ""); assertNotNull(items); assertEquals(1, ((List) items.get("data")).size()); @@ -206,13 +208,14 @@ public class SavingsAccountTransactionDatatableIntegrationTest { datatableEntryMap.put("dateFormat", "yyyy-MM-dd"); datatabelEntryRequestJsonString = new Gson().toJson(datatableEntryMap); HashMap updatedDatatableEntryResponse = this.datatableHelper.updateDatatableEntry(datatableName, transactionId, - false, datatabelEntryRequestJsonString); + datatableId, false, datatabelEntryRequestJsonString); - assertEquals(transactionId, updatedDatatableEntryResponse.get("resourceId")); + assertEquals(transactionId, Integer.valueOf((String) updatedDatatableEntryResponse.get("transactionId"))); + assertEquals(datatableId, updatedDatatableEntryResponse.get("resourceId")); // deleting datatable entries - Integer appTableId = this.datatableHelper.deleteDatatableEntries(datatableName, transactionId, "resourceId"); - assertEquals(transactionId, appTableId, "ERROR IN DELETING THE DATATABLE ENTRIES"); + String deletedTransactionId = (String) this.datatableHelper.deleteDatatableEntries(datatableName, transactionId, "transactionId"); + assertEquals(transactionId, Integer.valueOf(deletedTransactionId), "ERROR IN DELETING THE DATATABLE ENTRIES"); // deleting the datatable String deletedDataTableName = this.datatableHelper.deleteDatatable(datatableName); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/organisation/EntityDatatableChecksIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/organisation/EntityDatatableChecksIntegrationTest.java index 03c7661ed..507c28bc8 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/organisation/EntityDatatableChecksIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/organisation/EntityDatatableChecksIntegrationTest.java @@ -155,7 +155,7 @@ public class EntityDatatableChecksIntegrationTest { assertNotNull(entityDatatableCheckId, "ERROR IN DELETING THE ENTITY DATATABLE CHECK"); // deleting datatable entries - Integer appTableId = this.datatableHelper.deleteDatatableEntries(registeredTableName, clientID, "clientId"); + Integer appTableId = (Integer) this.datatableHelper.deleteDatatableEntries(registeredTableName, clientID, "clientId"); assertEquals(clientID, appTableId, "ERROR IN DELETING THE DATATABLE ENTRIES"); // deleting the datatable @@ -215,7 +215,7 @@ public class EntityDatatableChecksIntegrationTest { assertNotNull(entityDatatableCheckId, "ERROR IN DELETING THE ENTITY DATATABLE CHECK"); // deleting datatable entries - Integer appTableId = this.datatableHelper.deleteDatatableEntries(registeredTableName, groupId, "groupId"); + Integer appTableId = (Integer) this.datatableHelper.deleteDatatableEntries(registeredTableName, groupId, "groupId"); assertEquals(groupId, appTableId, "ERROR IN DELETING THE DATATABLE ENTRIES"); // deleting the datatable @@ -290,7 +290,7 @@ public class EntityDatatableChecksIntegrationTest { assertNotNull(entityDatatableCheckId, "ERROR IN DELETING THE ENTITY DATATABLE CHECK"); // deleting datatable entries - Integer appTableId = this.datatableHelper.deleteDatatableEntries(registeredTableName, savingsId, "savingsId"); + Integer appTableId = (Integer) this.datatableHelper.deleteDatatableEntries(registeredTableName, savingsId, "savingsId"); assertEquals(savingsId, appTableId, "ERROR IN DELETING THE DATATABLE ENTRIES"); // deleting the datatable @@ -372,7 +372,7 @@ public class EntityDatatableChecksIntegrationTest { assertNotNull(entityDatatableCheckId, "ERROR IN DELETING THE ENTITY DATATABLE CHECK"); // deleting datatable entries - Integer appTableId = this.datatableHelper.deleteDatatableEntries(registeredTableName, loanID, "loanId"); + Integer appTableId = (Integer) this.datatableHelper.deleteDatatableEntries(registeredTableName, loanID, "loanId"); assertEquals(loanID, appTableId, "ERROR IN DELETING THE DATATABLE ENTRIES"); // deleting the datatable diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/DatatableHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/DatatableHelper.java index f40e11a96..313494a7a 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/DatatableHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/DatatableHelper.java @@ -155,7 +155,7 @@ public class DatatableHelper extends IntegrationTest { DATATABLE_URL + "/" + datatableName + "?" + Utils.TENANT_IDENTIFIER, "resourceIdentifier"); } - public Integer deleteDatatableEntries(final String datatableName, final Integer apptableId, String jsonAttributeToGetBack) { + public Object deleteDatatableEntries(final String datatableName, final Integer apptableId, String jsonAttributeToGetBack) { final String deleteEntryUrl = DATATABLE_URL + "/" + datatableName + "/" + apptableId + "?genericResultSet=true" + "&" + Utils.TENANT_IDENTIFIER; return Utils.performServerDelete(this.requestSpec, this.responseSpec, deleteEntryUrl, jsonAttributeToGetBack); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableAdvancedQueryTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableAdvancedQueryTest.java index e47b1de62..87a6f98ea 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableAdvancedQueryTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableAdvancedQueryTest.java @@ -359,8 +359,8 @@ public class DatatableAdvancedQueryTest { private void deleteDatatable(String datatable, Integer... apptableIds) { for (Integer apptableId : apptableIds) { - Integer deletedId = this.datatableHelper.deleteDatatableEntries(datatable, apptableId, "resourceId"); - assertEquals(apptableId, deletedId, "ERROR IN DELETING THE DATATABLE ENTRY"); + String deletedId = (String) this.datatableHelper.deleteDatatableEntries(datatable, apptableId, "transactionId"); + assertEquals(apptableId, Integer.valueOf(deletedId), "ERROR IN DELETING THE DATATABLE ENTRY"); } String deletedDatatable = this.datatableHelper.deleteDatatable(datatable); assertEquals(datatable, deletedDatatable, "ERROR IN DELETING THE DATATABLE"); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableIntegrationTest.java index a66697575..8dd74135e 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableIntegrationTest.java @@ -333,7 +333,7 @@ public class DatatableIntegrationTest extends IntegrationTest { } // deleting datatable entries - Integer appTableId = this.datatableHelper.deleteDatatableEntries(datatableName, clientID, "clientId"); + Integer appTableId = (Integer) this.datatableHelper.deleteDatatableEntries(datatableName, clientID, "clientId"); assertEquals(clientID, appTableId, "ERROR IN DELETING THE DATATABLE ENTRIES"); // deleting the datatable @@ -434,7 +434,7 @@ public class DatatableIntegrationTest extends IntegrationTest { LOG.info("query result : {}", queryResult); // deleting datatable entries - Integer appTableId = this.datatableHelper.deleteDatatableEntries(datatableName, clientID, "clientId"); + Integer appTableId = (Integer) this.datatableHelper.deleteDatatableEntries(datatableName, clientID, "clientId"); assertEquals(clientID, appTableId, "ERROR IN DELETING THE DATATABLE ENTRIES"); // deleting the datatable @@ -662,7 +662,7 @@ public class DatatableIntegrationTest extends IntegrationTest { assertEquals(randomString, records.get(2)); assertEquals(textValue, records.get(5)); - Integer resourceId = this.datatableHelper.deleteDatatableEntries(datatableName, clientId, "resourceId"); + Integer resourceId = (Integer) this.datatableHelper.deleteDatatableEntries(datatableName, clientId, "resourceId"); assertEquals(clientId, resourceId, "ERROR IN DELETING THE DATATABLE ENTRIES"); // Update - update, delete DataTable columns @@ -915,7 +915,7 @@ public class DatatableIntegrationTest extends IntegrationTest { assertEquals(datatableEntryMap.get("itsAText"), datatableEntryResponseNoGenericResult.get(0).get("itsAText")); // deleting datatable entries - Integer appTableId = this.datatableHelper.deleteDatatableEntries(datatableName, loanID, "loanId"); + Integer appTableId = (Integer) this.datatableHelper.deleteDatatableEntries(datatableName, loanID, "loanId"); assertEquals(loanID, appTableId, "ERROR IN DELETING THE DATATABLE ENTRIES"); // deleting the datatable