Fix for reading multirow datatable entry reads non-belonging entry

This commit is contained in:
Jose Alberto Hernandez
2022-06-13 18:13:23 +02:00
committed by Arnold Gálovics
parent 8e37f93baf
commit 2b122a17ea
7 changed files with 140 additions and 48 deletions
@@ -82,7 +82,7 @@ public class AdHocScheduledJobRunnerServiceImpl implements AdHocScheduledJobRunn
run = Math.toIntExact(ChronoUnit.YEARS.between(start, end)) >= 1; run = Math.toIntExact(ChronoUnit.YEARS.between(start, end)) >= 1;
break; break;
case CUSTOM: case CUSTOM:
next = start.plusDays((int) (long) adhoc.getReportRunEvery()); next = start.plusDays((long) adhoc.getReportRunEvery());
run = Math.toIntExact(ChronoUnit.DAYS.between(start, end)) >= adhoc.getReportRunEvery(); run = Math.toIntExact(ChronoUnit.DAYS.between(start, end)) >= adhoc.getReportRunEvery();
break; break;
default: default:
@@ -38,7 +38,6 @@ public class CommandProcessingResult implements Serializable {
private final String transactionId; private final String transactionId;
private final Map<String, Object> changes; private final Map<String, Object> changes;
private final Map<String, Object> creditBureauReportData; private final Map<String, Object> creditBureauReportData;
@SuppressWarnings("unused")
private final String resourceIdentifier; private final String resourceIdentifier;
private final Long productId; private final Long productId;
private final Long gsimId; private final Long gsimId;
@@ -52,6 +51,13 @@ public class CommandProcessingResult implements Serializable {
commandResult.creditBureauReportData, commandResult.rollbackTransaction, commandResult.subResourceId); commandResult.creditBureauReportData, commandResult.rollbackTransaction, commandResult.subResourceId);
} }
public static CommandProcessingResult fromCommandProcessingResult(CommandProcessingResult commandResult, final Long resourceId) {
return new CommandProcessingResult(commandResult.commandId, commandResult.officeId, commandResult.groupId, commandResult.clientId,
commandResult.loanId, commandResult.savingsId, commandResult.resourceIdentifier, resourceId, commandResult.transactionId,
commandResult.changes, commandResult.productId, commandResult.gsimId, commandResult.glimId,
commandResult.creditBureauReportData, commandResult.rollbackTransaction, commandResult.subResourceId);
}
public static CommandProcessingResult fromDetails(final Long commandId, final Long officeId, final Long groupId, final Long clientId, public static CommandProcessingResult fromDetails(final Long commandId, final Long officeId, final Long groupId, final Long clientId,
final Long loanId, final Long savingsId, final String resourceIdentifier, final Long entityId, final Long gsimId, final Long loanId, final Long savingsId, final String resourceIdentifier, final Long entityId, final Long gsimId,
final Long glimId, final Map<String, Object> creditBureauReportData, final String transactionId, final Long glimId, final Map<String, Object> creditBureauReportData, final String transactionId,
@@ -21,7 +21,6 @@ package org.apache.fineract.infrastructure.dataqueries.handler;
import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -41,17 +40,6 @@ public class CreateDatatableEntryCommandHandler implements NewCommandSourceHandl
@Override @Override
public CommandProcessingResult processCommand(final JsonCommand command) { public CommandProcessingResult processCommand(final JsonCommand command) {
final CommandProcessingResult commandProcessingResult = this.writePlatformService.createNewDatatableEntry(command.entityName(), return this.writePlatformService.createNewDatatableEntry(command.entityName(), command.entityId(), command);
command.entityId(), command);
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();
} }
} }
@@ -26,6 +26,8 @@ import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
@@ -80,6 +82,8 @@ import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.BadSqlGrammarException; import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; 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.jdbc.support.rowset.SqlRowSet;
import org.springframework.jdbc.support.rowset.SqlRowSetMetaData; import org.springframework.jdbc.support.rowset.SqlRowSetMetaData;
import org.springframework.orm.jpa.JpaSystemException; import org.springframework.orm.jpa.JpaSystemException;
@@ -374,19 +378,30 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
public CommandProcessingResult createNewDatatableEntry(final String dataTableName, final Long appTableId, final String json) { public CommandProcessingResult createNewDatatableEntry(final String dataTableName, final Long appTableId, final String json) {
try { try {
final String appTable = queryForApplicationTableName(dataTableName); final String appTable = queryForApplicationTableName(dataTableName);
final CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(appTable, appTableId); CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(appTable, appTableId);
final List<ResultsetColumnHeaderData> columnHeaders = this.genericDataService.fillResultsetColumnHeaders(dataTableName); final List<ResultsetColumnHeaderData> columnHeaders = this.genericDataService.fillResultsetColumnHeaders(dataTableName);
final boolean multiRow = isMultirowDatatable(columnHeaders);
final Type typeOfMap = new TypeToken<Map<String, String>>() {}.getType(); final Type typeOfMap = new TypeToken<Map<String, String>>() {}.getType();
final Map<String, String> dataParams = this.fromJsonHelper.extractDataMap(typeOfMap, json); final Map<String, String> dataParams = this.fromJsonHelper.extractDataMap(typeOfMap, json);
final String sql = getAddSql(columnHeaders, dataTableName, getFKField(appTable), appTableId, dataParams); final String sql = getAddSql(columnHeaders, dataTableName, getFKField(appTable), appTableId, dataParams);
this.jdbcTemplate.update(sql); if (!multiRow) {
this.jdbcTemplate.update(sql);
commandProcessingResult = CommandProcessingResult.fromCommandProcessingResult(commandProcessingResult, appTableId);
} else {
final Long resourceId = addMultirowRecord(sql);
commandProcessingResult = CommandProcessingResult.fromCommandProcessingResult(commandProcessingResult, resourceId);
}
return commandProcessingResult; // 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) { } catch (final DataAccessException dve) {
final Throwable cause = dve.getCause(); final Throwable cause = dve.getCause();
final Throwable realCause = dve.getMostSpecificCause(); final Throwable realCause = dve.getMostSpecificCause();
@@ -423,7 +438,6 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
logAsErrorUnexpectedDataIntegrityException(e); logAsErrorUnexpectedDataIntegrityException(e);
throw new PlatformDataIntegrityException("error.msg.unknown.data.integrity.issue", throw new PlatformDataIntegrityException("error.msg.unknown.data.integrity.issue",
"Unknown data integrity issue with resource.", e); "Unknown data integrity issue with resource.", e);
} }
} }
@@ -693,6 +707,24 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withResourceIdAsString(datatableName).build(); return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withResourceIdAsString(datatableName).build();
} }
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 = null;
if (keyHolder.getKeys().size() > 1) {
assignedKey = (Long) keyHolder.getKeys().get("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, private void parseDatatableColumnForUpdate(final JsonObject column,
final Map<String, ResultsetColumnHeaderData> mapColumnNameDefinition, StringBuilder sqlBuilder, final String datatableName, final Map<String, ResultsetColumnHeaderData> mapColumnNameDefinition, StringBuilder sqlBuilder, final String datatableName,
final StringBuilder constrainBuilder, final Map<String, Long> codeMappings, final List<String> removeMappings, final StringBuilder constrainBuilder, final Map<String, Long> codeMappings, final List<String> removeMappings,
@@ -781,7 +813,6 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
} }
} }
@SuppressWarnings("deprecation")
private int getCodeIdForColumn(final String dataTableNameAlias, final String name) { private int getCodeIdForColumn(final String dataTableNameAlias, final String name) {
final StringBuilder checkColumnCodeMapping = new StringBuilder(); final StringBuilder checkColumnCodeMapping = new StringBuilder();
checkColumnCodeMapping.append("select ccm.code_id from x_table_column_code_mappings ccm where ccm.column_alias_name='") checkColumnCodeMapping.append("select ccm.code_id from x_table_column_code_mappings ccm where ccm.column_alias_name='")
@@ -1277,16 +1308,15 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
final List<ResultsetColumnHeaderData> columnHeaders = this.genericDataService.fillResultsetColumnHeaders(dataTableName); final List<ResultsetColumnHeaderData> columnHeaders = this.genericDataService.fillResultsetColumnHeaders(dataTableName);
String sql = ""; final boolean multiRow = isMultirowDatatable(columnHeaders);
// id only used for reading a specific entry in a one to many datatable String whereClause = getFKField(appTable) + " = " + appTableId;
// (when updating) SQLInjectionValidator.validateSQLInput(whereClause);
if (id == null) { String sql = "select * from " + sqlGenerator.escape(dataTableName) + " where " + whereClause;
String whereClause = getFKField(appTable) + " = " + appTableId;
SQLInjectionValidator.validateSQLInput(whereClause); // id only used for reading a specific entry that belongs to appTableId (in a one to many datatable)
sql = sql + "select * from " + sqlGenerator.escape(dataTableName) + " where " + whereClause; if (multiRow && id != null) {
} else { sql = sql + " and id = " + id;
sql = sql + "select * from " + sqlGenerator.escape(dataTableName) + " where id = " + id;
} }
if (StringUtils.isNotBlank(order)) { if (StringUtils.isNotBlank(order)) {
@@ -1304,16 +1334,15 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
final List<ResultsetColumnHeaderData> columnHeaders = this.genericDataService.fillResultsetColumnHeaders(dataTableName); final List<ResultsetColumnHeaderData> columnHeaders = this.genericDataService.fillResultsetColumnHeaders(dataTableName);
String sql = ""; final boolean multiRow = isMultirowDatatable(columnHeaders);
// id only used for reading a specific entry in a one to many datatable String whereClause = getFKField(appTable) + " = " + appTableId;
// (when updating) SQLInjectionValidator.validateSQLInput(whereClause);
if (id == null) { String sql = "select * from " + sqlGenerator.escape(dataTableName) + " where " + whereClause;
String whereClause = getFKField(appTable) + " = " + appTableId;
SQLInjectionValidator.validateSQLInput(whereClause); // id only used for reading a specific entry that belongs to appTableId (in a one to many datatable)
sql = sql + "select * from " + sqlGenerator.escape(dataTableName) + " where " + whereClause; if (multiRow && id != null) {
} else { sql = sql + " and id = " + id;
sql = sql + "select * from " + sqlGenerator.escape(dataTableName) + " where id = " + id;
} }
final List<ResultsetRowData> result = fillDatatableResultSetDataRows(sql); final List<ResultsetRowData> result = fillDatatableResultSetDataRows(sql);
@@ -1494,7 +1523,6 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
} }
private String getFKField(final String applicationTableName) { private String getFKField(final String applicationTableName) {
return applicationTableName.substring(2) + "_id"; return applicationTableName.substring(2) + "_id";
} }
@@ -1519,7 +1547,14 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
pValueWrite = "null"; pValueWrite = "null";
} else { } else {
if ("bit".equalsIgnoreCase(pColumnHeader.getColumnType())) { if ("bit".equalsIgnoreCase(pColumnHeader.getColumnType())) {
pValueWrite = BooleanUtils.toString(BooleanUtils.toBooleanObject(pValue), "1", "0", "null"); 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 { } else {
pValueWrite = singleQuote + this.genericDataService.replace(pValue, singleQuote, singleQuote + singleQuote) pValueWrite = singleQuote + this.genericDataService.replace(pValue, singleQuote, singleQuote + singleQuote)
+ singleQuote; + singleQuote;
@@ -1865,6 +1900,17 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
} }
private boolean isMultirowDatatable(final List<ResultsetColumnHeaderData> columnHeaders) {
boolean multiRow = false;
for (ResultsetColumnHeaderData column : columnHeaders) {
if (column.isNamed("id")) {
multiRow = true;
break;
}
}
return multiRow;
}
private boolean notTheSame(final String currValue, final String pValue, final String colType) { private boolean notTheSame(final String currValue, final String pValue, final String colType) {
if (StringUtils.isEmpty(currValue) && StringUtils.isEmpty(pValue)) { if (StringUtils.isEmpty(currValue) && StringUtils.isEmpty(pValue)) {
return false; return false;
@@ -1040,8 +1040,7 @@ public final class LoanApplicationTerms {
break; break;
case DAILY: case DAILY:
// For daily work out number of days in the period // For daily work out number of days in the period
BigDecimal numberOfDaysInPeriod = BigDecimal BigDecimal numberOfDaysInPeriod = BigDecimal.valueOf(ChronoUnit.DAYS.between(periodStartDate, periodEndDate));
.valueOf(Math.toIntExact(ChronoUnit.DAYS.between(periodStartDate, periodEndDate)));
final BigDecimal oneDayOfYearInterestRate = this.annualNominalInterestRate.divide(loanTermPeriodsInYearBigDecimal, mc) final BigDecimal oneDayOfYearInterestRate = this.annualNominalInterestRate.divide(loanTermPeriodsInYearBigDecimal, mc)
.divide(divisor, mc); .divide(divisor, mc);
@@ -52,11 +52,11 @@ public class DatatableHelper {
} }
public Integer createDatatableEntry(final String apptableName, final String datatableName, final Integer apptableId, public Integer createDatatableEntry(final String apptableName, final String datatableName, final Integer apptableId,
final boolean genericResultSet, final String dateFormat) { final boolean genericResultSet, final String dateFormat, final String jsonAttributeToGetBack) {
return Utils.performServerPost( return Utils.performServerPost(
this.requestSpec, this.responseSpec, DATATABLE_URL + "/" + datatableName + "/" + apptableId + "?genericResultSet=" this.requestSpec, this.responseSpec, DATATABLE_URL + "/" + datatableName + "/" + apptableId + "?genericResultSet="
+ Boolean.toString(genericResultSet) + "&" + Utils.TENANT_IDENTIFIER, + Boolean.toString(genericResultSet) + "&" + Utils.TENANT_IDENTIFIER,
getTestDatatableEntryAsJSON(dateFormat), "resourceId"); getTestDatatableEntryAsJSON(dateFormat), jsonAttributeToGetBack);
} }
public String readDatatableEntry(final String datatableName, final Integer resourceId, final boolean genericResultset) { public String readDatatableEntry(final String datatableName, final Integer resourceId, final boolean genericResultset) {
@@ -65,9 +65,16 @@ public class DatatableHelper {
} }
public List<String> readDatatableEntry(final String datatableName, final Integer resourceId, final boolean genericResultset, public List<String> readDatatableEntry(final String datatableName, final Integer resourceId, final boolean genericResultset,
final String jsonAttributeToGetBack) { final Integer datatableResourceId, final String jsonAttributeToGetBack) {
return Utils.performServerGetList(this.requestSpec, this.responseSpec, DATATABLE_URL + "/" + datatableName + "/" + resourceId if (datatableResourceId == null) {
+ "?genericResultSet=" + String.valueOf(genericResultset) + "&" + Utils.TENANT_IDENTIFIER, jsonAttributeToGetBack); return Utils.performServerGetList(this.requestSpec, this.responseSpec, DATATABLE_URL + "/" + datatableName + "/" + resourceId
+ "?genericResultSet=" + String.valueOf(genericResultset) + "&" + Utils.TENANT_IDENTIFIER, jsonAttributeToGetBack);
} else {
return Utils.performServerGetList(
this.requestSpec, this.responseSpec, DATATABLE_URL + "/" + datatableName + "/" + resourceId + "/" + datatableResourceId
+ "?genericResultSet=" + String.valueOf(genericResultset) + "&" + Utils.TENANT_IDENTIFIER,
jsonAttributeToGetBack);
}
} }
public Date readDatatableEntry(final String datatableName, final Integer resourceId, final boolean genericResultset, final int position, public Date readDatatableEntry(final String datatableName, final Integer resourceId, final boolean genericResultset, final int position,
@@ -63,11 +63,11 @@ public class DatatableIntegrationTest {
// creating new client datatable entry // creating new client datatable entry
final boolean genericResultSet = true; final boolean genericResultSet = true;
Integer datatableResourceID = this.datatableHelper.createDatatableEntry(CLIENT_APP_TABLE_NAME, datatableName, clientID, Integer datatableResourceID = this.datatableHelper.createDatatableEntry(CLIENT_APP_TABLE_NAME, datatableName, clientID,
genericResultSet, "yyyy-MM-dd"); genericResultSet, "yyyy-MM-dd", "resourceId");
assertNotNull(datatableResourceID, "ERROR IN CREATING THE ENTITY DATATABLE RECORD"); assertNotNull(datatableResourceID, "ERROR IN CREATING THE ENTITY DATATABLE RECORD");
// Read the Datatable entry generated with genericResultSet in true (default) // Read the Datatable entry generated with genericResultSet in true (default)
final List<String> items = this.datatableHelper.readDatatableEntry(datatableName, clientID, genericResultSet, "data"); final List<String> items = this.datatableHelper.readDatatableEntry(datatableName, clientID, genericResultSet, null, "data");
assertEquals(1, items.size()); assertEquals(1, items.size());
// Read the Datatable entry generated with genericResultSet in false // Read the Datatable entry generated with genericResultSet in false
@@ -84,4 +84,50 @@ public class DatatableIntegrationTest {
assertEquals(datatableName, deletedDataTableName, "ERROR IN DELETING THE DATATABLE"); assertEquals(datatableName, deletedDataTableName, "ERROR IN DELETING THE DATATABLE");
} }
@Test
public void validateReadDatatableMultirow() {
// creating multirow datatable for client entity
String datatableName = this.datatableHelper.createDatatable(CLIENT_APP_TABLE_NAME, true);
DatatableHelper.verifyDatatableCreatedOnServer(this.requestSpec, this.responseSpec, datatableName);
// creating first client with datatables
final Integer clientIdA = ClientHelper.createClientAsPerson(requestSpec, responseSpec);
// creating second client with datatables
final Integer clientIdB = ClientHelper.createClientAsPerson(requestSpec, responseSpec);
// creating new client datatable entry for first client
final boolean genericResultSet = true;
final Integer datatableResourceIdA = this.datatableHelper.createDatatableEntry(CLIENT_APP_TABLE_NAME, datatableName, clientIdA,
genericResultSet, "yyyy-MM-dd", "resourceId");
assertNotNull(datatableResourceIdA, "ERROR IN CREATING THE ENTITY DATATABLE RECORD");
// creating new client datatable entry for second client
final Integer datatableResourceIdB = this.datatableHelper.createDatatableEntry(CLIENT_APP_TABLE_NAME, datatableName, clientIdB,
genericResultSet, "yyyy-MM-dd", "resourceId");
assertNotNull(datatableResourceIdB, "ERROR IN CREATING THE ENTITY DATATABLE RECORD");
// Read the Datatable entry generated for first client
List<String> items;
items = this.datatableHelper.readDatatableEntry(datatableName, clientIdA, genericResultSet, datatableResourceIdA, "data");
assertEquals(1, items.size());
// Read the Datatable entry generated for second client
items = this.datatableHelper.readDatatableEntry(datatableName, clientIdB, genericResultSet, datatableResourceIdB, "data");
assertEquals(1, items.size());
// Read the Datatable entry generated for first client and second client's record Id
items = this.datatableHelper.readDatatableEntry(datatableName, clientIdA, genericResultSet, datatableResourceIdB, "data");
assertEquals(0, items.size());
// deleting datatable entries
Integer appTableIdA = this.datatableHelper.deleteDatatableEntries(datatableName, clientIdA, "clientId");
assertEquals(clientIdA, appTableIdA, "ERROR IN DELETING THE DATATABLE ENTRIES");
Integer appTableIdB = this.datatableHelper.deleteDatatableEntries(datatableName, clientIdB, "clientId");
assertEquals(clientIdB, appTableIdB, "ERROR IN DELETING THE DATATABLE ENTRIES");
// deleting the datatable
String deletedDataTableName = this.datatableHelper.deleteDatatable(datatableName);
assertEquals(datatableName, deletedDataTableName, "ERROR IN DELETING THE DATATABLE");
}
} }