FINERACT-1911: Assign Data Table to Transaction - general

This commit is contained in:
jmarta
2023-08-17 09:25:51 +02:00
committed by Adam Saghy
parent 2275f9c301
commit b616a96fc9
38 changed files with 2419 additions and 1360 deletions
@@ -18,13 +18,48 @@
*/
package org.apache.fineract.infrastructure.core.serialization;
import static org.apache.fineract.infrastructure.core.service.database.JdbcJavaType.BOOLEAN;
import static org.apache.fineract.infrastructure.core.service.database.JdbcJavaType.DATE;
import static org.apache.fineract.infrastructure.core.service.database.JdbcJavaType.DATETIME;
import static org.apache.fineract.infrastructure.core.service.database.JdbcJavaType.DECIMAL;
import static org.apache.fineract.infrastructure.core.service.database.JdbcJavaType.INTEGER;
import static org.apache.fineract.infrastructure.core.service.database.JdbcJavaType.TEXT;
import static org.apache.fineract.infrastructure.core.service.database.JdbcJavaType.VARCHAR;
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;
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_FIELD_LENGTH;
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_FIELD_MANDATORY;
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_FIELD_NAME;
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_FIELD_NEWCODE;
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_FIELD_NEWNAME;
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_FIELD_TYPE;
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_FIELD_TYPE_BOOLEAN;
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_FIELD_TYPE_DATE;
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_FIELD_TYPE_DATETIME;
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_FIELD_TYPE_DECIMAL;
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_FIELD_TYPE_DROPDOWN;
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_FIELD_TYPE_NUMBER;
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_FIELD_TYPE_STRING;
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_FIELD_TYPE_TEXT;
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_FIELD_TYPE_TIMESTAMP;
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_FIELD_UNIQUE;
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_PARAM_ADDCOLUMNS;
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_PARAM_APPTABLE_NAME;
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_PARAM_CHANGECOLUMNS;
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_PARAM_COLUMNS;
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_PARAM_DATATABLE_NAME;
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.TABLE_FIELD_ID;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import jakarta.validation.constraints.NotNull;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -34,6 +69,8 @@ import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver;
import org.apache.fineract.infrastructure.core.service.database.JdbcJavaType;
import org.apache.fineract.infrastructure.dataqueries.data.EntityTables;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -41,90 +78,33 @@ import org.springframework.stereotype.Component;
@Component
public class DatatableCommandFromApiJsonDeserializer {
public static final String DATATABLE_NAME = "datatableName";
public static final String ENTITY_SUB_TYPE = "entitySubType";
public static final String APPTABLE_NAME = "apptableName";
public static final String MULTI_ROW = "multiRow";
public static final String COLUMNS = "columns";
public static final String NAME = "name";
public static final String TYPE = "type";
public static final String LENGTH = "length";
public static final String MANDATORY = "mandatory";
public static final String CODE = "code";
public static final String CHANGE_COLUMNS = "changeColumns";
public static final String ADD_COLUMNS = "addColumns";
public static final String DROP_COLUMNS = "dropColumns";
public static final String AFTER = "after";
public static final String NEW_CODE = "newCode";
public static final String NEW_NAME = "newName";
public static final String STRING = "string";
public static final String NUMBER = "number";
public static final String BOOLEAN = "boolean";
public static final String DECIMAL = "decimal";
public static final String DATE = "date";
public static final String DATETIME = "datetime";
public static final String TEXT = "text";
public static final String DROPDOWN = "dropdown";
private static final String DATATABLE_NAME_REGEX_PATTERN = "^[a-zA-Z][a-zA-Z0-9\\-_\\s]{0,48}[a-zA-Z0-9]$";
private static final String DATATABLE_COLUMN_NAME_REGEX_PATTERN = "^[a-zA-Z][a-zA-Z0-9\\-_\\s]{0,}[a-zA-Z0-9]$";
private static final String INDEXED = "indexed";
private static final String UNIQUE = "unique";
public static final String DATATABLE_NAME_REGEX_PATTERN = "^[a-zA-Z][a-zA-Z0-9\\-_\\s]{0,48}[a-zA-Z0-9]$";
public static final String DATATABLE_COLUMN_NAME_REGEX_PATTERN = "^[a-zA-Z][a-zA-Z0-9\\-_\\s]{0,}[a-zA-Z0-9]$";
/**
* The parameters supported for this command.
*/
private static final Set<String> SUPPORTED_PARAMETERS_FOR_CREATE = new HashSet<>(
Arrays.asList(DATATABLE_NAME, ENTITY_SUB_TYPE, APPTABLE_NAME, MULTI_ROW, COLUMNS));
private static final Set<String> SUPPORTED_PARAMETERS_FOR_CREATE_COLUMNS = new HashSet<>(
Arrays.asList(NAME, TYPE, LENGTH, MANDATORY, CODE, UNIQUE, INDEXED));
private static final Set<String> SUPPORTED_PARAMETERS_FOR_UPDATE = new HashSet<>(
Arrays.asList(APPTABLE_NAME, ENTITY_SUB_TYPE, CHANGE_COLUMNS, ADD_COLUMNS, DROP_COLUMNS));
private static final Set<String> SUPPORTED_PARAMETERS_FOR_ADD_COLUMNS = new HashSet<>(
Arrays.asList(NAME, TYPE, LENGTH, MANDATORY, AFTER, CODE, UNIQUE, INDEXED));
private static final Set<String> SUPPORTED_PARAMETERS_FOR_CHANGE_COLUMNS = new HashSet<>(
Arrays.asList(NAME, NEW_NAME, LENGTH, MANDATORY, AFTER, CODE, NEW_CODE, UNIQUE, INDEXED));
private static final Set<String> SUPPORTED_PARAMETERS_FOR_DROP_COLUMNS = new HashSet<>(List.of(NAME));
private static final Object[] SUPPORTED_COLUMN_TYPES = { STRING, NUMBER, BOOLEAN, DECIMAL, DATE, DATETIME, TEXT, DROPDOWN };
private static final Set<String> SUPPORTED_PARAMETERS_FOR_CREATE = Set.of(API_PARAM_DATATABLE_NAME, API_PARAM_SUBTYPE,
API_PARAM_APPTABLE_NAME, API_PARAM_MULTIROW, API_PARAM_COLUMNS);
private static final Set<String> SUPPORTED_PARAMETERS_FOR_CREATE_COLUMNS = Set.of(API_FIELD_NAME, API_FIELD_TYPE, API_FIELD_LENGTH,
API_FIELD_MANDATORY, API_FIELD_CODE, API_FIELD_UNIQUE, API_FIELD_INDEXED);
private static final Set<String> SUPPORTED_PARAMETERS_FOR_UPDATE = Set.of(API_PARAM_APPTABLE_NAME, API_PARAM_SUBTYPE,
API_PARAM_CHANGECOLUMNS, API_PARAM_ADDCOLUMNS, API_PARAM_DROPCOLUMNS);
private static final Set<String> SUPPORTED_PARAMETERS_FOR_ADD_COLUMNS = Set.of(API_FIELD_NAME, API_FIELD_TYPE, API_FIELD_LENGTH,
API_FIELD_MANDATORY, API_FIELD_AFTER, API_FIELD_CODE, API_FIELD_UNIQUE, API_FIELD_INDEXED);
private static final Set<String> SUPPORTED_PARAMETERS_FOR_CHANGE_COLUMNS = Set.of(API_FIELD_NAME, API_FIELD_NEWNAME, API_FIELD_LENGTH,
API_FIELD_MANDATORY, API_FIELD_AFTER, API_FIELD_CODE, API_FIELD_NEWCODE, API_FIELD_UNIQUE, API_FIELD_INDEXED);
private static final Set<String> SUPPORTED_PARAMETERS_FOR_DROP_COLUMNS = Set.of(API_FIELD_NAME);
private static final Object[] SUPPORTED_COLUMN_TYPES = { API_FIELD_TYPE_STRING, API_FIELD_TYPE_NUMBER, API_FIELD_TYPE_BOOLEAN,
API_FIELD_TYPE_DECIMAL, API_FIELD_TYPE_DATE, API_FIELD_TYPE_DATETIME, API_FIELD_TYPE_TEXT, API_FIELD_TYPE_DROPDOWN };
private final FromJsonHelper fromApiJsonHelper;
private final DatabaseTypeResolver databaseTypeResolver;
@Autowired
public DatatableCommandFromApiJsonDeserializer(final FromJsonHelper fromApiJsonHelper) {
public DatatableCommandFromApiJsonDeserializer(final FromJsonHelper fromApiJsonHelper, DatabaseTypeResolver databaseTypeResolver) {
this.fromApiJsonHelper = fromApiJsonHelper;
}
private void validateType(final DataValidatorBuilder baseDataValidator, final JsonElement column) {
final String type = this.fromApiJsonHelper.extractStringNamed(TYPE, column);
baseDataValidator.reset().parameter(TYPE).value(type).notBlank().isOneOfTheseStringValues(SUPPORTED_COLUMN_TYPES);
if (type != null && type.equalsIgnoreCase("String")) {
if (this.fromApiJsonHelper.parameterExists(LENGTH, column)) {
final String lengthStr = this.fromApiJsonHelper.extractStringNamed(LENGTH, column);
if (lengthStr != null && !StringUtils.isWhitespace(lengthStr) && StringUtils.isNumeric(lengthStr)
&& StringUtils.isNotBlank(lengthStr)) {
final Integer length = Integer.parseInt(lengthStr);
baseDataValidator.reset().parameter(LENGTH).value(length).positiveAmount();
} else if (StringUtils.isBlank(lengthStr) || StringUtils.isWhitespace(lengthStr)) {
baseDataValidator.reset().parameter(LENGTH).failWithCode("must.be.provided.when.type.is.String");
} else if (!StringUtils.isNumeric(lengthStr)) {
baseDataValidator.reset().parameter(LENGTH).failWithCode("not.greater.than.zero");
}
} else {
baseDataValidator.reset().parameter(LENGTH).failWithCode("must.be.provided.when.type.is.String");
}
} else {
baseDataValidator.reset().parameter(LENGTH).mustBeBlankWhenParameterProvidedIs(TYPE, type);
}
final String code = this.fromApiJsonHelper.extractStringNamed(CODE, column);
if (type != null && type.equalsIgnoreCase(DROPDOWN)) {
if (code != null) {
baseDataValidator.reset().parameter(CODE).value(code).notBlank().matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN);
} else {
baseDataValidator.reset().parameter(CODE).value(code).cantBeBlankWhenParameterProvidedIs(TYPE, type);
}
} else {
baseDataValidator.reset().parameter(CODE).value(code).mustBeBlankWhenParameterProvided(TYPE, type);
}
this.databaseTypeResolver = databaseTypeResolver;
}
public void validateForCreate(final String json) {
@@ -132,9 +112,7 @@ public class DatatableCommandFromApiJsonDeserializer {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
}.getType();
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS_FOR_CREATE);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
@@ -142,53 +120,50 @@ public class DatatableCommandFromApiJsonDeserializer {
final JsonElement element = this.fromApiJsonHelper.parse(json);
final String datatableName = this.fromApiJsonHelper.extractStringNamed(DATATABLE_NAME, element);
baseDataValidator.reset().parameter(DATATABLE_NAME).value(datatableName).notBlank().notExceedingLengthOf(50)
final String datatableName = this.fromApiJsonHelper.extractStringNamed(API_PARAM_DATATABLE_NAME, element);
baseDataValidator.reset().parameter(API_PARAM_DATATABLE_NAME).value(datatableName).notBlank().notExceedingLengthOf(50)
.matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN);
final String apptableName = this.fromApiJsonHelper.extractStringNamed(APPTABLE_NAME, element);
baseDataValidator.reset().parameter(APPTABLE_NAME).value(apptableName).notBlank().notExceedingLengthOf(50)
.isOneOfTheseStringValues(EntityTables.getEntitiesList());
final String apptableName = this.fromApiJsonHelper.extractStringNamed(API_PARAM_APPTABLE_NAME, element);
baseDataValidator.reset().parameter(API_PARAM_APPTABLE_NAME).value(apptableName).notBlank().notExceedingLengthOf(50)
.isOneOfTheseStringValues(EntityTables.getEntityNames());
validateEntitySubType(baseDataValidator, element, apptableName);
final String fkColumnName = (apptableName != null) ? apptableName.substring(2) + "_id" : "";
EntityTables entityTable = EntityTables.fromEntityName(apptableName);
validateEntitySubType(baseDataValidator, element, entityTable);
final Boolean multiRow = this.fromApiJsonHelper.extractBooleanNamed(MULTI_ROW, element);
baseDataValidator.reset().parameter(MULTI_ROW).value(multiRow).ignoreIfNull().notBlank().isOneOfTheseValues(true, false);
final String fkColumnName = entityTable.getForeignKeyColumnNameOnDatatable();
final JsonArray columns = this.fromApiJsonHelper.extractJsonArrayNamed(COLUMNS, element);
baseDataValidator.reset().parameter(COLUMNS).value(columns).notNull().jsonArrayNotEmpty();
final Boolean multiRow = this.fromApiJsonHelper.extractBooleanNamed(API_PARAM_MULTIROW, element);
baseDataValidator.reset().parameter(API_PARAM_MULTIROW).value(multiRow).ignoreIfNull().notBlank().isOneOfTheseValues(true, false);
final JsonArray columns = this.fromApiJsonHelper.extractJsonArrayNamed(API_PARAM_COLUMNS, element);
baseDataValidator.reset().parameter(API_PARAM_COLUMNS).value(columns).notNull().jsonArrayNotEmpty();
if (columns != null) {
for (final JsonElement column : columns) {
this.fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), SUPPORTED_PARAMETERS_FOR_CREATE_COLUMNS);
final String name = this.fromApiJsonHelper.extractStringNamed(NAME, column);
baseDataValidator.reset().parameter(NAME).value(name).notBlank().isNotOneOfTheseValues("id", fkColumnName)
.matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN);
final String name = this.fromApiJsonHelper.extractStringNamed(API_FIELD_NAME, column);
baseDataValidator.reset().parameter(API_FIELD_NAME).value(name).notBlank()
.isNotOneOfTheseValues(TABLE_FIELD_ID, fkColumnName).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN);
validateType(baseDataValidator, column);
final Boolean mandatory = this.fromApiJsonHelper.extractBooleanNamed(MANDATORY, column);
final Boolean unique = this.fromApiJsonHelper.extractBooleanNamed(UNIQUE, column);
final Boolean indexed = this.fromApiJsonHelper.extractBooleanNamed(INDEXED, column);
baseDataValidator.reset().parameter(MANDATORY).value(mandatory).ignoreIfNull().notBlank().isOneOfTheseValues(true, false);
baseDataValidator.reset().parameter(UNIQUE).value(unique).ignoreIfNull().notBlank().isOneOfTheseValues(true, false);
baseDataValidator.reset().parameter(INDEXED).value(indexed).ignoreIfNull().notBlank().isOneOfTheseValues(true, false);
final Boolean mandatory = this.fromApiJsonHelper.extractBooleanNamed(API_FIELD_MANDATORY, column);
final Boolean unique = this.fromApiJsonHelper.extractBooleanNamed(API_FIELD_UNIQUE, column);
final Boolean indexed = this.fromApiJsonHelper.extractBooleanNamed(API_FIELD_INDEXED, column);
baseDataValidator.reset().parameter(API_FIELD_MANDATORY).value(mandatory).ignoreIfNull().notBlank().isOneOfTheseValues(true,
false);
baseDataValidator.reset().parameter(API_FIELD_UNIQUE).value(unique).ignoreIfNull().notBlank().isOneOfTheseValues(true,
false);
baseDataValidator.reset().parameter(API_FIELD_INDEXED).value(indexed).ignoreIfNull().notBlank().isOneOfTheseValues(true,
false);
}
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
private void validateEntitySubType(final DataValidatorBuilder baseDataValidator, final JsonElement element, final String apptableName) {
EntityTables entityTable = EntityTables.fromName(apptableName);
if (entityTable != null && EntityTables.CLIENT == entityTable) {
String entitySubType = this.fromApiJsonHelper.extractStringNamed(ENTITY_SUB_TYPE, element);
baseDataValidator.reset().parameter(ENTITY_SUB_TYPE).value(entitySubType).notBlank(); // Person or Entity
}
}
public void validateForUpdate(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
@@ -211,108 +186,167 @@ public class DatatableCommandFromApiJsonDeserializer {
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("datatable");
final JsonElement element = this.fromApiJsonHelper.parse(json);
final String apptableName = this.fromApiJsonHelper.extractStringNamed(APPTABLE_NAME, element);
baseDataValidator.reset().parameter(APPTABLE_NAME).value(apptableName).ignoreIfNull().notBlank()
.isOneOfTheseStringValues(EntityTables.getEntitiesList());
final String apptableName = this.fromApiJsonHelper.extractStringNamed(API_PARAM_APPTABLE_NAME, element);
baseDataValidator.reset().parameter(API_PARAM_APPTABLE_NAME).value(apptableName).ignoreIfNull().notBlank()
.isOneOfTheseStringValues(EntityTables.getEntityNames());
validateEntitySubType(baseDataValidator, element, apptableName);
EntityTables entityTable = EntityTables.fromEntityName(apptableName);
validateEntitySubType(baseDataValidator, element, entityTable);
final String fkColumnName = (apptableName != null) ? apptableName.substring(2) + "_id" : "";
final String fkColumnName = entityTable.getForeignKeyColumnNameOnDatatable();
final JsonArray changeColumns = this.fromApiJsonHelper.extractJsonArrayNamed(CHANGE_COLUMNS, element);
baseDataValidator.reset().parameter(CHANGE_COLUMNS).value(changeColumns).ignoreIfNull().jsonArrayNotEmpty();
final JsonArray changeColumns = this.fromApiJsonHelper.extractJsonArrayNamed(API_PARAM_CHANGECOLUMNS, element);
baseDataValidator.reset().parameter(API_PARAM_CHANGECOLUMNS).value(changeColumns).ignoreIfNull().jsonArrayNotEmpty();
if (changeColumns != null) {
for (final JsonElement column : changeColumns) {
this.fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), SUPPORTED_PARAMETERS_FOR_CHANGE_COLUMNS);
final String name = this.fromApiJsonHelper.extractStringNamed(NAME, column);
baseDataValidator.reset().parameter(NAME).value(name).notBlank().isNotOneOfTheseValues("id", fkColumnName)
.matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN);
final String name = this.fromApiJsonHelper.extractStringNamed(API_FIELD_NAME, column);
baseDataValidator.reset().parameter(API_FIELD_NAME).value(name).notBlank()
.isNotOneOfTheseValues(TABLE_FIELD_ID, fkColumnName).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN);
final String newName = this.fromApiJsonHelper.extractStringNamed(NEW_NAME, column);
baseDataValidator.reset().parameter(NEW_NAME).value(newName).ignoreIfNull().notBlank().notExceedingLengthOf(50)
.isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN);
final String newName = this.fromApiJsonHelper.extractStringNamed(API_FIELD_NEWNAME, column);
baseDataValidator.reset().parameter(API_FIELD_NEWNAME).value(newName).ignoreIfNull().notBlank().notExceedingLengthOf(50)
.isNotOneOfTheseValues(TABLE_FIELD_ID, fkColumnName).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN);
if (this.fromApiJsonHelper.parameterExists(LENGTH, column)) {
final String lengthStr = this.fromApiJsonHelper.extractStringNamed(LENGTH, column);
if (this.fromApiJsonHelper.parameterExists(API_FIELD_LENGTH, column)) {
final String lengthStr = this.fromApiJsonHelper.extractStringNamed(API_FIELD_LENGTH, column);
if (StringUtils.isWhitespace(lengthStr) || !StringUtils.isNumeric(lengthStr) || StringUtils.isBlank(lengthStr)) {
baseDataValidator.reset().parameter(LENGTH).failWithCode("not.greater.than.zero");
baseDataValidator.reset().parameter(API_FIELD_LENGTH).failWithCode("not.greater.than.zero");
} else {
final Integer length = Integer.parseInt(lengthStr);
baseDataValidator.reset().parameter(LENGTH).value(length).ignoreIfNull().notBlank().positiveAmount();
baseDataValidator.reset().parameter(API_FIELD_LENGTH).value(length).ignoreIfNull().notBlank().positiveAmount();
}
}
final String code = this.fromApiJsonHelper.extractStringNamed(CODE, column);
baseDataValidator.reset().parameter(CODE).value(code).ignoreIfNull().notBlank().notExceedingLengthOf(100)
final String code = this.fromApiJsonHelper.extractStringNamed(API_FIELD_CODE, column);
baseDataValidator.reset().parameter(API_FIELD_CODE).value(code).ignoreIfNull().notBlank().notExceedingLengthOf(100)
.matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN);
final String newCode = this.fromApiJsonHelper.extractStringNamed(NEW_CODE, column);
baseDataValidator.reset().parameter(NEW_CODE).value(newCode).ignoreIfNull().notBlank().notExceedingLengthOf(100)
final String newCode = this.fromApiJsonHelper.extractStringNamed(API_FIELD_NEWCODE, column);
baseDataValidator.reset().parameter(API_FIELD_NEWCODE).value(newCode).ignoreIfNull().notBlank().notExceedingLengthOf(100)
.matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN);
if (StringUtils.isBlank(code) && StringUtils.isNotBlank(newCode)) {
baseDataValidator.reset().parameter(CODE).value(code).cantBeBlankWhenParameterProvidedIs(NEW_CODE, newCode);
baseDataValidator.reset().parameter(API_FIELD_CODE).value(code).cantBeBlankWhenParameterProvidedIs(API_FIELD_NEWCODE,
newCode);
}
final Boolean mandatory = this.fromApiJsonHelper.extractBooleanNamed(MANDATORY, column);
baseDataValidator.reset().parameter(MANDATORY).value(mandatory).ignoreIfNull().notBlank().isOneOfTheseValues(true, false);
final Boolean mandatory = this.fromApiJsonHelper.extractBooleanNamed(API_FIELD_MANDATORY, column);
baseDataValidator.reset().parameter(API_FIELD_MANDATORY).value(mandatory).ignoreIfNull().notBlank().isOneOfTheseValues(true,
false);
final Boolean after = this.fromApiJsonHelper.extractBooleanNamed(AFTER, column);
baseDataValidator.reset().parameter(AFTER).value(after).ignoreIfNull().notBlank().isOneOfTheseValues(true, false);
final Boolean after = this.fromApiJsonHelper.extractBooleanNamed(API_FIELD_AFTER, column);
baseDataValidator.reset().parameter(API_FIELD_AFTER).value(after).ignoreIfNull().notBlank().isOneOfTheseValues(true, false);
final Boolean unique = this.fromApiJsonHelper.extractBooleanNamed(UNIQUE, column);
baseDataValidator.reset().parameter(UNIQUE).value(unique).ignoreIfNull().notBlank().isOneOfTheseValues(true, false);
final Boolean unique = this.fromApiJsonHelper.extractBooleanNamed(API_FIELD_UNIQUE, column);
baseDataValidator.reset().parameter(API_FIELD_UNIQUE).value(unique).ignoreIfNull().notBlank().isOneOfTheseValues(true,
false);
final Boolean indexed = this.fromApiJsonHelper.extractBooleanNamed(INDEXED, column);
baseDataValidator.reset().parameter(INDEXED).value(indexed).ignoreIfNull().notBlank().isOneOfTheseValues(true, false);
final Boolean indexed = this.fromApiJsonHelper.extractBooleanNamed(API_FIELD_INDEXED, column);
baseDataValidator.reset().parameter(API_FIELD_INDEXED).value(indexed).ignoreIfNull().notBlank().isOneOfTheseValues(true,
false);
}
}
final JsonArray addColumns = this.fromApiJsonHelper.extractJsonArrayNamed(ADD_COLUMNS, element);
baseDataValidator.reset().parameter(ADD_COLUMNS).value(addColumns).ignoreIfNull().jsonArrayNotEmpty();
final JsonArray addColumns = this.fromApiJsonHelper.extractJsonArrayNamed(API_PARAM_ADDCOLUMNS, element);
baseDataValidator.reset().parameter(API_PARAM_ADDCOLUMNS).value(addColumns).ignoreIfNull().jsonArrayNotEmpty();
if (addColumns != null) {
for (final JsonElement column : addColumns) {
this.fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), SUPPORTED_PARAMETERS_FOR_ADD_COLUMNS);
final String name = this.fromApiJsonHelper.extractStringNamed(NAME, column);
baseDataValidator.reset().parameter(NAME).value(name).notBlank().isNotOneOfTheseValues("id", fkColumnName)
.matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN);
final String name = this.fromApiJsonHelper.extractStringNamed(API_FIELD_NAME, column);
baseDataValidator.reset().parameter(API_FIELD_NAME).value(name).notBlank()
.isNotOneOfTheseValues(TABLE_FIELD_ID, fkColumnName).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN);
validateType(baseDataValidator, column);
final Boolean mandatory = this.fromApiJsonHelper.extractBooleanNamed(MANDATORY, column);
baseDataValidator.reset().parameter(MANDATORY).value(mandatory).ignoreIfNull().notBlank().isOneOfTheseValues(true, false);
final Boolean mandatory = this.fromApiJsonHelper.extractBooleanNamed(API_FIELD_MANDATORY, column);
baseDataValidator.reset().parameter(API_FIELD_MANDATORY).value(mandatory).ignoreIfNull().notBlank().isOneOfTheseValues(true,
false);
final Boolean after = this.fromApiJsonHelper.extractBooleanNamed(AFTER, column);
baseDataValidator.reset().parameter(AFTER).value(after).ignoreIfNull().notBlank().isOneOfTheseValues(true, false);
final Boolean after = this.fromApiJsonHelper.extractBooleanNamed(API_FIELD_AFTER, column);
baseDataValidator.reset().parameter(API_FIELD_AFTER).value(after).ignoreIfNull().notBlank().isOneOfTheseValues(true, false);
final Boolean unique = this.fromApiJsonHelper.extractBooleanNamed(UNIQUE, column);
baseDataValidator.reset().parameter(UNIQUE).value(unique).ignoreIfNull().notBlank().isOneOfTheseValues(true, false);
final Boolean unique = this.fromApiJsonHelper.extractBooleanNamed(API_FIELD_UNIQUE, column);
baseDataValidator.reset().parameter(API_FIELD_UNIQUE).value(unique).ignoreIfNull().notBlank().isOneOfTheseValues(true,
false);
final Boolean indexed = this.fromApiJsonHelper.extractBooleanNamed(INDEXED, column);
baseDataValidator.reset().parameter(INDEXED).value(indexed).ignoreIfNull().notBlank().isOneOfTheseValues(true, false);
final Boolean indexed = this.fromApiJsonHelper.extractBooleanNamed(API_FIELD_INDEXED, column);
baseDataValidator.reset().parameter(API_FIELD_INDEXED).value(indexed).ignoreIfNull().notBlank().isOneOfTheseValues(true,
false);
}
}
final JsonArray dropColumns = this.fromApiJsonHelper.extractJsonArrayNamed(DROP_COLUMNS, element);
baseDataValidator.reset().parameter(DROP_COLUMNS).value(dropColumns).ignoreIfNull().jsonArrayNotEmpty();
final JsonArray dropColumns = this.fromApiJsonHelper.extractJsonArrayNamed(API_PARAM_DROPCOLUMNS, element);
baseDataValidator.reset().parameter(API_PARAM_DROPCOLUMNS).value(dropColumns).ignoreIfNull().jsonArrayNotEmpty();
if (dropColumns != null) {
for (final JsonElement column : dropColumns) {
this.fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), SUPPORTED_PARAMETERS_FOR_DROP_COLUMNS);
final String name = this.fromApiJsonHelper.extractStringNamed(NAME, column);
baseDataValidator.reset().parameter(NAME).value(name).notBlank().isNotOneOfTheseValues("id", fkColumnName)
.matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN);
final String name = this.fromApiJsonHelper.extractStringNamed(API_FIELD_NAME, column);
baseDataValidator.reset().parameter(API_FIELD_NAME).value(name).notBlank()
.isNotOneOfTheseValues(TABLE_FIELD_ID, fkColumnName).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN);
}
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
private void validateType(DataValidatorBuilder validator, final JsonElement column) {
final String type = this.fromApiJsonHelper.extractStringNamed(API_FIELD_TYPE, column);
validator.reset().parameter(API_FIELD_TYPE).value(type).notBlank().isOneOfTheseStringValues(SUPPORTED_COLUMN_TYPES);
if (type == null) {
return;
}
final Integer length = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(API_FIELD_LENGTH, column);
JdbcJavaType jdbcType = mapApiTypeToJdbcType(type);
validator = validator.reset().parameter(API_FIELD_LENGTH).value(length);
if (jdbcType.hasPrecision(databaseTypeResolver.databaseType())) {
if (jdbcType.isStringType() && length == null) {
validator.failWithCode("must.be.provided.when.type.is.String");
}
validator.ignoreIfNull().positiveAmount();
} // else, the precision is ignored
final String code = this.fromApiJsonHelper.extractStringNamed(API_FIELD_CODE, column);
if (type.equalsIgnoreCase(API_FIELD_TYPE_DROPDOWN)) {
if (code != null) {
validator.reset().parameter(API_FIELD_CODE).value(code).notBlank().matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN);
} else {
validator.reset().parameter(API_FIELD_CODE).value(code).cantBeBlankWhenParameterProvidedIs(API_FIELD_TYPE, type);
}
} else {
validator.reset().parameter(API_FIELD_CODE).value(code).mustBeBlankWhenParameterProvided(API_FIELD_TYPE, type);
}
}
@NotNull
public static JdbcJavaType mapApiTypeToJdbcType(@NotNull String apiType) {
return switch (apiType.toLowerCase()) {
case API_FIELD_TYPE_STRING -> VARCHAR;
case API_FIELD_TYPE_NUMBER, API_FIELD_TYPE_DROPDOWN -> INTEGER;
case API_FIELD_TYPE_BOOLEAN -> BOOLEAN;
case API_FIELD_TYPE_DECIMAL -> DECIMAL;
case API_FIELD_TYPE_DATE -> DATE;
case API_FIELD_TYPE_DATETIME, API_FIELD_TYPE_TIMESTAMP -> DATETIME;
case API_FIELD_TYPE_TEXT -> TEXT;
default -> throw new PlatformDataIntegrityException("error.msg.datatable.column.type.invalid",
"Column type " + apiType + " is not supported.");
};
}
private void validateEntitySubType(final DataValidatorBuilder baseDataValidator, final JsonElement element,
final EntityTables entityTable) {
if (entityTable == EntityTables.CLIENT) {
String entitySubType = this.fromApiJsonHelper.extractStringNamed(API_PARAM_SUBTYPE, element);
baseDataValidator.reset().parameter(API_PARAM_SUBTYPE).value(entitySubType).notBlank(); // Person or Entity
}
}
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
@@ -37,9 +37,6 @@ public final class DataTableApiConstant {
public static final String CREATEDAT_FIELD_NAME = "created_at";
public static final String UPDATEDAT_FIELD_NAME = "updated_at";
// Field Types
public static final String DATETIME_FIELD_TYPE = "DateTime";
// associationParameters
public static final String allAssociateParamName = "all";
public static final String repaymentScheduleAssociateParamName = "repaymentSchedule";
@@ -55,4 +52,38 @@ public final class DataTableApiConstant {
public static final String meetingAssociateParamName = "meeting";
public static final String emiAmountVariationsAssociateParamName = "emiAmountVariations";
public static final String collectionAssociateParamName = "collection";
public static final String TABLE_COLUMN_CODE_MAPPINGS = "x_table_column_code_mappings";
public static final String TABLE_REGISTERED_TABLE = "x_registered_table";
public static final String TABLE_FIELD_ID = "id";
public static final String API_PARAM_COLUMNS = "columns";
public static final String API_PARAM_CHANGECOLUMNS = "changeColumns";
public static final String API_PARAM_ADDCOLUMNS = "addColumns";
public static final String API_PARAM_DROPCOLUMNS = "dropColumns";
public static final String API_PARAM_DATATABLE_NAME = "datatableName";
public static final String API_PARAM_APPTABLE_NAME = "apptableName";
public static final String API_PARAM_SUBTYPE = "entitySubType";
public static final String API_PARAM_MULTIROW = "multiRow";
public static final String API_FIELD_NAME = "name";
public static final String API_FIELD_NEWNAME = "newName";
public static final String API_FIELD_TYPE = "type";
public static final String API_FIELD_LENGTH = "length";
public static final String API_FIELD_MANDATORY = "mandatory";
public static final String API_FIELD_UNIQUE = "unique";
public static final String API_FIELD_INDEXED = "indexed";
public static final String API_FIELD_AFTER = "after";
public static final String API_FIELD_CODE = "code";
public static final String API_FIELD_NEWCODE = "newCode";
public static final String API_FIELD_TYPE_STRING = "string";
public static final String API_FIELD_TYPE_NUMBER = "number";
public static final String API_FIELD_TYPE_BOOLEAN = "boolean";
public static final String API_FIELD_TYPE_DECIMAL = "decimal";
public static final String API_FIELD_TYPE_DATE = "date";
public static final String API_FIELD_TYPE_DATETIME = "datetime";
public static final String API_FIELD_TYPE_TIMESTAMP = "timestamp";
public static final String API_FIELD_TYPE_TEXT = "text";
public static final String API_FIELD_TYPE_DROPDOWN = "dropdown";
}
@@ -18,88 +18,114 @@
*/
package org.apache.fineract.infrastructure.dataqueries.data;
import static org.apache.fineract.infrastructure.dataqueries.data.StatusEnum.ACTIVATE;
import static org.apache.fineract.infrastructure.dataqueries.data.StatusEnum.APPROVE;
import static org.apache.fineract.infrastructure.dataqueries.data.StatusEnum.CLOSE;
import static org.apache.fineract.infrastructure.dataqueries.data.StatusEnum.CREATE;
import static org.apache.fineract.infrastructure.dataqueries.data.StatusEnum.DISBURSE;
import static org.apache.fineract.infrastructure.dataqueries.data.StatusEnum.REJECTED;
import static org.apache.fineract.infrastructure.dataqueries.data.StatusEnum.WITHDRAWN;
import static org.apache.fineract.infrastructure.dataqueries.data.StatusEnum.WRITE_OFF;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.HashMap;
import jakarta.validation.constraints.NotNull;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public enum EntityTables {
CLIENT("m_client", ImmutableList.of(StatusEnum.CREATE.getCode(), StatusEnum.ACTIVATE.getCode(), StatusEnum.CLOSE.getCode()),
"client_id"), LOAN("m_loan",
ImmutableList.of(StatusEnum.CREATE.getCode(), StatusEnum.APPROVE.getCode(), StatusEnum.DISBURSE.getCode(),
StatusEnum.WITHDRAWN.getCode(), StatusEnum.REJECTED.getCode(), StatusEnum.WRITE_OFF.getCode()),
"loan_id"), GROUP("m_group",
ImmutableList.of(StatusEnum.CREATE.getCode(), StatusEnum.ACTIVATE.getCode(), StatusEnum.CLOSE.getCode()),
"group_id"), SAVING("m_savings_account",
ImmutableList.of(StatusEnum.CREATE.getCode(), StatusEnum.APPROVE.getCode(),
StatusEnum.ACTIVATE.getCode(), StatusEnum.WITHDRAWN.getCode(), StatusEnum.REJECTED.getCode(),
StatusEnum.CLOSE.getCode()),
"savings_account_id"), SAVINGS_TRANSACTION("m_savings_account_transaction", ImmutableList.of(),
"savings_account_transcation_id"), OFFICE("m_office", ImmutableList.of(),
"office_id"), PRODUCT_LOAN("m_product_loan", ImmutableList.of(),
"product_loan_id"), SAVINGS_PRODUCT("m_savings_product", ImmutableList.of(),
"savings_product_id"), SHARE_PRODUCT("m_share_product",
ImmutableList.of(), "share_product_id");
CLIENT("m_client", "client_id", "id", CREATE, ACTIVATE, CLOSE), //
GROUP("m_group", "group_id", "id", CREATE, ACTIVATE, CLOSE), //
CENTER("m_center", "m_group", "center_id", "id"), //
OFFICE("m_office", "office_id", "id"), //
LOAN_PRODUCT("m_product_loan", "product_loan_id", "id"), //
LOAN("m_loan", "loan_id", "id", CREATE, APPROVE, DISBURSE, WITHDRAWN, REJECTED, WRITE_OFF), //
SAVINGS_PRODUCT("m_savings_product", "savings_product_id", "id"), //
SAVINGS("m_savings_account", "savings_account_id", "id", CREATE, APPROVE, ACTIVATE, WITHDRAWN, REJECTED, CLOSE), //
SAVINGS_TRANSACTION("m_savings_account_transaction", "savings_transaction_id", "id"), //
SHARE_PRODUCT("m_share_product", "share_product_id", "id"), //
;
private static final Map<String, EntityTables> lookup = new HashMap<String, EntityTables>();
public static final EntityTables[] VALUES = values();
static {
for (EntityTables d : EntityTables.values()) {
lookup.put(d.getName(), d);
}
}
private static final List<String> ENTITY_NAMES = Arrays.stream(VALUES).map(EntityTables::getName).toList();
private static final Map<String, EntityTables> BY_ENTITY_NAME = Arrays.stream(VALUES)
.collect(Collectors.toMap(EntityTables::getName, e -> e));
private final String name;
private final ImmutableList<Integer> codes;
private final String apptableName;
private final String foreignKeyColumnNameOnDatatable;
private final String refColumn; // referenced column name on apptable
EntityTables(String name, ImmutableList<Integer> codes, String foreignKeyColumnNameOnDatatable) {
private final ImmutableList<StatusEnum> checkStatuses;
EntityTables(String name, String apptableName, String foreignKeyColumnNameOnDatatable, String refColumn, StatusEnum... statuses) {
this.name = name;
this.codes = codes;
this.apptableName = apptableName;
this.foreignKeyColumnNameOnDatatable = foreignKeyColumnNameOnDatatable;
this.refColumn = refColumn;
this.checkStatuses = statuses == null ? ImmutableList.of() : ImmutableList.copyOf(statuses);
}
public static List<String> getEntitiesList() {
List<String> data = new ArrayList<String>();
for (EntityTables entity : EntityTables.values()) {
data.add(entity.name);
}
return data;
}
public static ImmutableList<Integer> getStatus(String name) {
if (lookup.get(name) != null) {
return lookup.get(name).getCodes();
}
return ImmutableList.of();
}
public static EntityTables fromName(String name) {
return name == null ? null : lookup.get(name.toLowerCase());
}
public ImmutableList<Integer> getCodes() {
return this.codes;
EntityTables(String name, String foreignKeyColumnNameOnDatatable, String refColumn, StatusEnum... statuses) {
this(name, name, foreignKeyColumnNameOnDatatable, refColumn, statuses);
}
public String getName() {
return name;
}
public String getApptableName() {
return apptableName;
}
public String getForeignKeyColumnNameOnDatatable() {
return this.foreignKeyColumnNameOnDatatable;
}
public static String getForeignKeyColumnNameOnDatatable(String name) {
return lookup.get(name).foreignKeyColumnNameOnDatatable;
public String getRefColumn() {
return refColumn;
}
public List<StatusEnum> getCheckStatuses() {
return checkStatuses;
}
public boolean hasCheck() {
return checkStatuses != null && !checkStatuses.isEmpty();
}
public static List<String> getEntityNames() {
return ENTITY_NAMES;
}
public static EntityTables fromEntityName(String name) {
return name == null ? null : BY_ENTITY_NAME.get(name.toLowerCase());
}
public static String getForeignKeyColumnNameOnDatatable(String name) {
EntityTables entityTable = fromEntityName(name);
return entityTable == null ? null : entityTable.getForeignKeyColumnNameOnDatatable();
}
@NotNull
public static List<StatusEnum> getCheckStatuses(String name) {
EntityTables entityTable = fromEntityName(name);
return entityTable == null ? List.of() : entityTable.getCheckStatuses();
}
@NotNull
public static List<Integer> getCheckStatusCodes(String name) {
return getCheckStatuses(name).stream().map(StatusEnum::getCode).toList();
}
@NotNull
public static List<EntityTables> getFiltered(Predicate<EntityTables> filter) {
return Arrays.stream(VALUES).filter(filter).toList();
}
}
@@ -19,6 +19,7 @@
package org.apache.fineract.infrastructure.dataqueries.data;
import java.util.List;
import org.apache.fineract.infrastructure.core.service.database.JdbcJavaType;
/**
* Immutable data object for generic resultset data.
@@ -41,16 +42,13 @@ public final class GenericResultsetData {
return this.data;
}
public String getColTypeOfColumnNamed(final String columnName) {
String colType = null;
public JdbcJavaType getColTypeOfColumnNamed(final String columnName) {
for (final ResultsetColumnHeaderData columnHeader : this.columnHeaders) {
if (columnHeader.isNamed(columnName)) {
colType = columnHeader.getColumnType();
return columnHeader.getColumnType();
}
}
return colType;
return null;
}
public boolean hasNoEntries() {
@@ -19,6 +19,7 @@
package org.apache.fineract.infrastructure.dataqueries.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
import org.apache.fineract.infrastructure.dataqueries.data.EntityTables;
/**
* A {@link RuntimeException} thrown when datatable resources are not found.
@@ -32,4 +33,12 @@ public class DatatableNotFoundException extends AbstractPlatformResourceNotFound
public DatatableNotFoundException(final String datatable) {
super("error.msg.datatable.not.found", "Datatable not found.", datatable);
}
public DatatableNotFoundException(final EntityTables entityTable, final Long id) {
this(entityTable.getName(), id);
}
public DatatableNotFoundException(final EntityTables entityTable) {
this(entityTable.getName());
}
}
@@ -18,6 +18,8 @@
*/
package org.apache.fineract.infrastructure.dataqueries.service;
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_PARAM_DATATABLE_NAME;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
@@ -41,7 +43,6 @@ import org.springframework.stereotype.Component;
public final class EntityDatatableChecksDataValidator {
public static final String ENTITY = "entity";
public static final String DATATABLE_NAME = "datatableName";
public static final String STATUS = "status";
public static final String SYSTEM_DEFINED = "systemDefined";
public static final String PRODUCT_ID = "productId";
@@ -50,7 +51,7 @@ public final class EntityDatatableChecksDataValidator {
* The parameters supported for this command.
*/
private static final Set<String> SUPPORTED_PARAMETERS = new HashSet<>(
Arrays.asList(ENTITY, DATATABLE_NAME, STATUS, SYSTEM_DEFINED, PRODUCT_ID));
Arrays.asList(ENTITY, API_PARAM_DATATABLE_NAME, STATUS, SYSTEM_DEFINED, PRODUCT_ID));
private final FromJsonHelper fromApiJsonHelper;
@Autowired
@@ -74,15 +75,15 @@ public final class EntityDatatableChecksDataValidator {
final JsonElement element = this.fromApiJsonHelper.parse(json);
final String entity = this.fromApiJsonHelper.extractStringNamed(ENTITY, element);
baseDataValidator.reset().parameter(ENTITY).value(entity).notBlank().isOneOfTheseStringValues(EntityTables.getEntitiesList());
baseDataValidator.reset().parameter(ENTITY).value(entity).notBlank().isOneOfTheseStringValues(EntityTables.getEntityNames());
final Integer status = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(STATUS, element);
final Object[] entityTablesStatuses = EntityTables.getStatus(entity).toArray();
final Object[] entityTablesStatuses = EntityTables.getCheckStatusCodes(entity).toArray();
baseDataValidator.reset().parameter(STATUS).value(status).isOneOfTheseValues(entityTablesStatuses);
final String datatableName = this.fromApiJsonHelper.extractStringNamed(DATATABLE_NAME, element);
baseDataValidator.reset().parameter(DATATABLE_NAME).value(datatableName).notBlank();
final String datatableName = this.fromApiJsonHelper.extractStringNamed(API_PARAM_DATATABLE_NAME, element);
baseDataValidator.reset().parameter(API_PARAM_DATATABLE_NAME).value(datatableName).notBlank();
if (this.fromApiJsonHelper.parameterExists(SYSTEM_DEFINED, element)) {
final String systemDefined = this.fromApiJsonHelper.extractStringNamed(SYSTEM_DEFINED, element);
@@ -18,7 +18,6 @@
*/
package org.apache.fineract.infrastructure.dataqueries.service;
import com.google.common.collect.ImmutableList;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
@@ -30,6 +29,7 @@ import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.core.service.PaginationHelper;
import org.apache.fineract.infrastructure.core.service.SearchParameters;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver;
import org.apache.fineract.infrastructure.dataqueries.data.DatatableCheckStatusData;
import org.apache.fineract.infrastructure.dataqueries.data.DatatableChecksData;
import org.apache.fineract.infrastructure.dataqueries.data.DatatableData;
@@ -52,6 +52,7 @@ import org.springframework.stereotype.Service;
public class EntityDatatableChecksReadPlatformServiceImpl implements EntityDatatableChecksReadService {
private final JdbcTemplate jdbcTemplate;
protected final DatabaseTypeResolver databaseTypeResolver;
private final DatabaseSpecificSQLGenerator sqlGenerator;
private final RegisterDataTableMapper registerDataTableMapper;
private final EntityDataTableChecksMapper entityDataTableChecksMapper;
@@ -62,14 +63,14 @@ public class EntityDatatableChecksReadPlatformServiceImpl implements EntityDatat
private final PaginationHelper paginationHelper;
@Autowired
public EntityDatatableChecksReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate,
final LoanProductReadPlatformService loanProductReadPlatformService,
public EntityDatatableChecksReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, DatabaseTypeResolver databaseTypeResolver,
DatabaseSpecificSQLGenerator sqlGenerator, final LoanProductReadPlatformService loanProductReadPlatformService,
final SavingsProductReadPlatformService savingsProductReadPlatformService,
final EntityDatatableChecksRepository entityDatatableChecksRepository,
final ReadWriteNonCoreDataService readWriteNonCoreDataService, DatabaseSpecificSQLGenerator sqlGenerator,
PaginationHelper paginationHelper) {
final ReadWriteNonCoreDataService readWriteNonCoreDataService, PaginationHelper paginationHelper) {
this.jdbcTemplate = jdbcTemplate;
this.databaseTypeResolver = databaseTypeResolver;
this.sqlGenerator = sqlGenerator;
this.registerDataTableMapper = new RegisterDataTableMapper();
this.entityDataTableChecksMapper = new EntityDataTableChecksMapper();
@@ -141,28 +142,25 @@ public class EntityDatatableChecksReadPlatformServiceImpl implements EntityDatat
@Override
public EntityDataTableChecksTemplateData retrieveTemplate() {
List<DatatableChecksData> dataTables = getDataTables();
List<String> entities = EntityTables.getEntitiesList();
List<DatatableCheckStatusData> statusClient = getStatusList(EntityTables.getStatus("m_client"));
List<DatatableCheckStatusData> statusLoan = getStatusList(EntityTables.getStatus("m_loan"));
List<DatatableCheckStatusData> statusGroup = getStatusList(EntityTables.getStatus("m_group"));
List<DatatableCheckStatusData> statusSavings = getStatusList(EntityTables.getStatus("m_savings_account"));
List<String> entities = EntityTables.getEntityNames();
List<DatatableCheckStatusData> clientStatuses = getStatusList(EntityTables.CLIENT.getCheckStatuses());
List<DatatableCheckStatusData> loanStatuses = getStatusList(EntityTables.LOAN.getCheckStatuses());
List<DatatableCheckStatusData> groupstatuses = getStatusList(EntityTables.GROUP.getCheckStatuses());
List<DatatableCheckStatusData> savingsStatuses = getStatusList(EntityTables.SAVINGS.getCheckStatuses());
Collection<LoanProductData> loanProductDatas = this.loanProductReadPlatformService.retrieveAllLoanProductsForLookup(true);
Collection<SavingsProductData> savingsProductDatas = this.savingsProductReadPlatformService.retrieveAllForLookup();
return new EntityDataTableChecksTemplateData(entities, statusClient, statusGroup, statusSavings, statusLoan, dataTables,
return new EntityDataTableChecksTemplateData(entities, clientStatuses, groupstatuses, savingsStatuses, loanStatuses, dataTables,
loanProductDatas, savingsProductDatas);
}
private List<DatatableCheckStatusData> getStatusList(ImmutableList<Integer> statuses) {
private List<DatatableCheckStatusData> getStatusList(List<StatusEnum> statuses) {
List<DatatableCheckStatusData> ret = new ArrayList<>();
if (statuses != null) {
for (Integer status : statuses) {
StatusEnum statusEnum = StatusEnum.fromInt(status);
ret.add(new DatatableCheckStatusData(statusEnum.name(), statusEnum.getCode()));
for (StatusEnum status : statuses) {
ret.add(new DatatableCheckStatusData(status.name(), status.getCode()));
}
}
return ret;
@@ -69,7 +69,6 @@ public class EntityDatatableChecksWritePlatformServiceImpl implements EntityData
@Transactional
@Override
public CommandProcessingResult createCheck(final JsonCommand command) {
try {
this.context.authenticatedUser();
@@ -105,11 +104,11 @@ public class EntityDatatableChecksWritePlatformServiceImpl implements EntityData
throw new EntityDatatableCheckAlreadyExistsException(entity, status, datatableName);
}
} else {
EntityTables entityTable = EntityTables.fromName(entity);
if (EntityTables.LOAN == entityTable) {
EntityTables entityTable = EntityTables.fromEntityName(entity);
if (entityTable == EntityTables.LOAN) {
// if invalid loan product id, throws exception
this.loanProductReadPlatformService.retrieveLoanProduct(productId);
} else if (EntityTables.SAVING == entityTable) {
} else if (entityTable == EntityTables.SAVINGS) {
// if invalid savings product id, throws exception
this.savingsProductReadPlatformService.retrieveOne(productId);
} else {
@@ -21,20 +21,23 @@ package org.apache.fineract.infrastructure.dataqueries.service;
import java.util.List;
import org.apache.fineract.infrastructure.dataqueries.data.GenericResultsetData;
import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData;
import org.apache.fineract.infrastructure.dataqueries.data.ResultsetRowData;
public interface GenericDataService {
GenericResultsetData fillGenericResultSet(String sql);
List<ResultsetColumnHeaderData> fillResultsetColumnHeaders(String tableName);
List<ResultsetRowData> fillResultsetRowData(String sql, List<ResultsetColumnHeaderData> columnHeaders);
String generateJsonFromGenericResultsetData(GenericResultsetData grs);
String replace(String str, String pattern, String replace);
String wrapSQL(String sql);
List<ResultsetColumnHeaderData> fillResultsetColumnHeaders(String datatable);
boolean isExplicitlyUnique(String tableName, String columnName);
boolean isExplicitlyUnique(String datatable, String columnName);
boolean isExplicitlyIndexed(String datatable, String columnName);
boolean isExplicitlyIndexed(String tableName, String columnName);
}
@@ -19,23 +19,35 @@
package org.apache.fineract.infrastructure.dataqueries.service;
import static java.lang.String.format;
import static org.apache.fineract.infrastructure.core.service.database.JdbcJavaType.DATE;
import static org.apache.fineract.infrastructure.core.service.database.JdbcJavaType.DATETIME;
import static org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData.DisplayType.CODELOOKUP;
import static org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData.DisplayType.DECIMAL;
import static org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData.DisplayType.INTEGER;
import static org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData.DisplayType.TIME;
import java.sql.Date;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.service.database.DatabaseIndependentQueryService;
import org.apache.fineract.infrastructure.core.service.database.DatabaseType;
import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver;
import org.apache.fineract.infrastructure.core.service.database.IndexDetail;
import org.apache.fineract.infrastructure.core.service.database.JdbcJavaType;
import org.apache.fineract.infrastructure.core.service.database.RoutingDataSource;
import org.apache.fineract.infrastructure.dataqueries.data.GenericResultsetData;
import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData;
import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnValueData;
import org.apache.fineract.infrastructure.dataqueries.data.ResultsetRowData;
import org.apache.fineract.infrastructure.dataqueries.exception.DatatableNotFoundException;
import org.jetbrains.annotations.NotNull;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.rowset.SqlRowSet;
@@ -51,6 +63,7 @@ public class GenericDataServiceImpl implements GenericDataService {
private final RoutingDataSource dataSource;
private final DatabaseIndependentQueryService databaseIndependentQueryService;
private final DatatableKeywordGenerator datatableKeywordGenerator;
private final DatabaseTypeResolver databaseTypeResolver;
@Override
public GenericResultsetData fillGenericResultSet(final String sql) {
@@ -58,38 +71,18 @@ public class GenericDataServiceImpl implements GenericDataService {
final SqlRowSet rs = this.jdbcTemplate.queryForRowSet(sql); // NOSONAR
final List<ResultsetColumnHeaderData> columnHeaders = new ArrayList<>();
final List<ResultsetRowData> resultsetDataRows = new ArrayList<>();
final SqlRowSetMetaData rsmd = rs.getMetaData();
for (int i = 0; i < rsmd.getColumnCount(); i++) {
final String columnName = rsmd.getColumnName(i + 1);
final String columnType = rsmd.getColumnTypeName(i + 1);
final ResultsetColumnHeaderData columnHeader = ResultsetColumnHeaderData.basic(columnName, columnType);
final ResultsetColumnHeaderData columnHeader = ResultsetColumnHeaderData.basic(columnName, columnType,
databaseTypeResolver.databaseType());
columnHeaders.add(columnHeader);
}
while (rs.next()) {
final List<Object> columnValues = new ArrayList<>();
for (int i = 0; i < rsmd.getColumnCount(); i++) {
final String columnName = rsmd.getColumnName(i + 1);
final String colType = columnHeaders.get(i).getColumnType();
if ("DATE".equalsIgnoreCase(colType)) {
java.sql.Date tmpDate = (java.sql.Date) rs.getObject(columnName);
columnValues.add(tmpDate != null ? tmpDate.toLocalDate() : null);
} else if ("TIMESTAMP WITHOUT TIME ZONE".equalsIgnoreCase(colType) // PostgreSQL
|| "DATETIME".equalsIgnoreCase(colType) || "TIMESTAMP".equalsIgnoreCase(colType)) {
Timestamp tmpDate = (Timestamp) rs.getObject(columnName);
columnValues.add(tmpDate != null ? tmpDate.toLocalDateTime() : null);
} else {
columnValues.add(rs.getObject(columnName));
}
}
final ResultsetRowData resultsetDataRow = ResultsetRowData.create(columnValues);
resultsetDataRows.add(resultsetDataRow);
}
final List<ResultsetRowData> resultsetDataRows = fillResultsetRowData(rs, columnHeaders);
return new GenericResultsetData(columnHeaders, resultsetDataRows);
} catch (DataAccessException e) {
@@ -98,6 +91,80 @@ public class GenericDataServiceImpl implements GenericDataService {
}
}
@Override
public List<ResultsetColumnHeaderData> fillResultsetColumnHeaders(final String tableName) {
final SqlRowSet columnDefinitions = getTableMetaData(tableName);
final List<IndexDetail> indexDefinitions = getDatatableIndexData(tableName);
DatabaseType dialect = databaseTypeResolver.databaseType();
final List<ResultsetColumnHeaderData> columnHeaders = new ArrayList<>();
columnDefinitions.beforeFirst();
while (columnDefinitions.next()) {
final String columnName = columnDefinitions.getString(1);
final String isNullable = columnDefinitions.getString(2);
final String isPrimaryKey = columnDefinitions.getString(5);
final String columnType = columnDefinitions.getString(3);
final Long columnLength = columnDefinitions.getLong(4);
final boolean columnNullable = "YES".equalsIgnoreCase(isNullable) || "TRUE".equalsIgnoreCase(isNullable);
final boolean columnIsPrimaryKey = "PRI".equalsIgnoreCase(isPrimaryKey) || "TRUE".equalsIgnoreCase(isPrimaryKey);
// primary keys are automatically unique
final boolean columnIsUnique = columnIsPrimaryKey || isExplicitlyUnique(tableName, columnName, indexDefinitions);
// primary keys and unique constrained columns are automatically indexed
final boolean columnIsIndexed = columnIsPrimaryKey || columnIsUnique
|| isExplicitlyIndexed(tableName, columnName, indexDefinitions);
JdbcJavaType jdbcType = JdbcJavaType.getByTypeName(dialect, columnType);
List<ResultsetColumnValueData> columnValues = new ArrayList<>();
String codeName = null;
final int codePosition = columnName.indexOf("_cd");
if (codePosition > 0 && jdbcType != null && (jdbcType.isVarcharType() || jdbcType.isIntegerType())) {
codeName = columnName.substring(0, codePosition);
columnValues = retrieveCodeValues(codeName);
}
columnHeaders.add(ResultsetColumnHeaderData.detailed(columnName, columnType, columnLength, columnNullable, columnIsPrimaryKey,
columnValues, codeName, columnIsUnique, columnIsIndexed, dialect));
}
return columnHeaders;
}
@NotNull
@Override
public List<ResultsetRowData> fillResultsetRowData(final String sql, List<ResultsetColumnHeaderData> columnHeaders) {
final SqlRowSet rs = jdbcTemplate.queryForRowSet(sql); // NOSONAR
return fillResultsetRowData(rs, columnHeaders);
}
@NotNull
private static List<ResultsetRowData> fillResultsetRowData(SqlRowSet rs, List<ResultsetColumnHeaderData> columnHeaders) {
final SqlRowSetMetaData rsmd = rs.getMetaData();
final List<ResultsetRowData> resultsetDataRows = new ArrayList<>();
while (rs.next()) {
final List<Object> columnValues = new ArrayList<>();
for (int i = 0; i < rsmd.getColumnCount(); i++) {
final String columnName = rsmd.getColumnName(i + 1);
final JdbcJavaType colType = columnHeaders.get(i).getColumnType();
if (colType == DATE) {
Date tmpDate = (Date) rs.getObject(columnName);
columnValues.add(tmpDate == null ? null : tmpDate.toLocalDate());
} else if (colType == DATETIME) {
Object tmpDate = rs.getObject(columnName);
columnValues.add(
tmpDate == null ? null : (tmpDate instanceof Timestamp ? ((Timestamp) tmpDate).toLocalDateTime() : tmpDate));
} else {
columnValues.add(rs.getObject(columnName));
}
}
resultsetDataRows.add(ResultsetRowData.create(columnValues));
}
return resultsetDataRows;
}
@Override
public String replace(final String str, final String pattern, final String replace) {
// JPW - this replace may / may not be any better or quicker than the
@@ -129,7 +196,6 @@ public class GenericDataServiceImpl implements GenericDataService {
@Override
public String generateJsonFromGenericResultsetData(final GenericResultsetData grs) {
final StringBuilder writer = new StringBuilder();
writer.append("[");
@@ -141,7 +207,7 @@ public class GenericDataServiceImpl implements GenericDataService {
Integer rSize;
final String doubleQuote = "\"";
final String slashDoubleQuote = "\\\"";
String currColType;
ResultsetColumnHeaderData.DisplayType colDisplayType;
Object currVal;
for (int i = 0; i < data.size(); i++) {
@@ -150,39 +216,31 @@ public class GenericDataServiceImpl implements GenericDataService {
row = data.get(i).getRow();
rSize = row.size();
for (int j = 0; j < rSize; j++) {
writer.append(doubleQuote + columnHeaders.get(j).getColumnName() + doubleQuote + ": ");
currColType = columnHeaders.get(j).getColumnDisplayType();
final String colType = columnHeaders.get(j).getColumnType();
if (currColType == null && colType.equalsIgnoreCase("INT")) {
currColType = "INTEGER";
}
if (currColType == null && colType.equalsIgnoreCase("VARCHAR")) {
currColType = "VARCHAR";
}
if (currColType == null && colType.equalsIgnoreCase("DATE")) {
currColType = "DATE";
}
if (currColType == null && colType.equalsIgnoreCase("DATETIME")) {
currColType = "DATETIME";
ResultsetColumnHeaderData columnHeader = columnHeaders.get(j);
writer.append(doubleQuote + columnHeader.getColumnName() + doubleQuote + ": ");
colDisplayType = columnHeader.getColumnDisplayType();
final JdbcJavaType colType = columnHeader.getColumnType();
if (colDisplayType == null) {
colDisplayType = ResultsetColumnHeaderData.calcColumnDisplayType(colType);
}
currVal = row.get(j);
if (currVal != null && currColType != null) {
if (currColType.equalsIgnoreCase("DECIMAL") || currColType.equalsIgnoreCase("INTEGER")
|| currColType.equalsIgnoreCase("CODELOOKUP")) {
if (currVal != null && colDisplayType != null) {
if (colDisplayType == ResultsetColumnHeaderData.DisplayType.DATE) {
final LocalDate localDate = (LocalDate) currVal;
writer.append(format("[%d,%d,%d]", localDate.getYear(), localDate.getMonthValue(), localDate.getDayOfMonth()));
} else if (colDisplayType == ResultsetColumnHeaderData.DisplayType.DATETIME) {
final LocalDateTime localDateTime = (LocalDateTime) currVal;
writer.append(format("[%d,%d,%d,%d,%d,%d,%d]", localDateTime.getYear(), localDateTime.getMonthValue(),
localDateTime.getDayOfMonth(), localDateTime.getHour(), localDateTime.getMinute(),
localDateTime.getSecond(), localDateTime.getNano()));
} else if (colDisplayType == TIME) {
final LocalTime localTime = (LocalTime) currVal;
writer.append(format("[%d,%d,%d,%d]", localTime.getHour(), localTime.getMinute(), localTime.getSecond(),
localTime.getNano()));
} else if (colDisplayType == DECIMAL || colDisplayType == INTEGER || colDisplayType == CODELOOKUP) {
writer.append(currVal);
} else {
if (currColType.equalsIgnoreCase("DATE")) {
final LocalDate localDate = (LocalDate) currVal;
writer.append(format("[%d,%d,%d]", localDate.getYear(), localDate.getMonthValue(), localDate.getDayOfMonth()));
} else if (currColType.equalsIgnoreCase("DATETIME")) {
final LocalDateTime localDateTime = (LocalDateTime) currVal;
writer.append(format("[%d,%d,%d,%d,%d,%d,%d]", localDateTime.getYear(), localDateTime.getMonthValue(),
localDateTime.getDayOfMonth(), localDateTime.getHour(), localDateTime.getMinute(),
localDateTime.getSecond(), localDateTime.getNano()));
} else {
writer.append(doubleQuote + replace(String.valueOf(currVal), doubleQuote, slashDoubleQuote) + doubleQuote);
}
writer.append(doubleQuote + replace(String.valueOf(currVal), doubleQuote, slashDoubleQuote) + doubleQuote);
}
} else {
writer.append("null");
@@ -201,69 +259,26 @@ public class GenericDataServiceImpl implements GenericDataService {
writer.append("\n]");
return writer.toString();
}
@Override
public List<ResultsetColumnHeaderData> fillResultsetColumnHeaders(final String datatable) {
final SqlRowSet columnDefinitions = getDatatableMetaData(datatable);
final List<IndexDetail> indexDefinitions = getDatatableIndexData(datatable);
final List<ResultsetColumnHeaderData> columnHeaders = new ArrayList<>();
columnDefinitions.beforeFirst();
while (columnDefinitions.next()) {
final String columnName = columnDefinitions.getString(1);
final String isNullable = columnDefinitions.getString(2);
final String isPrimaryKey = columnDefinitions.getString(5);
final String columnType = columnDefinitions.getString(3);
final Long columnLength = columnDefinitions.getLong(4);
final boolean columnNullable = "YES".equalsIgnoreCase(isNullable) || "TRUE".equalsIgnoreCase(isNullable);
final boolean columnIsPrimaryKey = "PRI".equalsIgnoreCase(isPrimaryKey) || "TRUE".equalsIgnoreCase(isPrimaryKey);
// primary keys are automatically unique
final boolean columnIsUnique = columnIsPrimaryKey || isExplicitlyUnique(datatable, columnName, indexDefinitions);
// primary keys and unique constrained columns are automatically indexed
final boolean columnIsIndexed = columnIsPrimaryKey || columnIsUnique
|| isExplicitlyIndexed(datatable, columnName, indexDefinitions);
List<ResultsetColumnValueData> columnValues = new ArrayList<>();
String codeName = null;
final int codePosition = columnName.indexOf("_cd");
if ("varchar".equalsIgnoreCase(columnType) || "int".equalsIgnoreCase(columnType) || "integer".equalsIgnoreCase(columnType)) {
if (codePosition > 0) {
codeName = columnName.substring(0, codePosition);
columnValues = retrieveColumnValues(codeName);
}
}
columnHeaders.add(ResultsetColumnHeaderData.detailed(columnName, columnType, columnLength, columnNullable, columnIsPrimaryKey,
columnValues, codeName, columnIsUnique, columnIsIndexed));
}
return columnHeaders;
}
private boolean isExplicitlyUnique(String datatable, String columnName, List<IndexDetail> indexDefinitions) {
String keyNameToCheck = datatableKeywordGenerator.generateUniqueKeyName(datatable, columnName);
private boolean isExplicitlyUnique(String tableName, String columnName, List<IndexDetail> indexDefinitions) {
String keyNameToCheck = datatableKeywordGenerator.generateUniqueKeyName(tableName, columnName);
return checkKeyPresent(keyNameToCheck, indexDefinitions);
}
@Override
public boolean isExplicitlyUnique(String datatable, String columnName) {
return isExplicitlyUnique(datatable, columnName, getDatatableIndexData(datatable));
public boolean isExplicitlyUnique(String tableName, String columnName) {
return isExplicitlyUnique(tableName, columnName, getDatatableIndexData(tableName));
}
private boolean isExplicitlyIndexed(String datatable, String columnName, List<IndexDetail> indexDefinitions) {
String keyNameToCheck = datatableKeywordGenerator.generateIndexName(datatable, columnName);
private boolean isExplicitlyIndexed(String tableName, String columnName, List<IndexDetail> indexDefinitions) {
String keyNameToCheck = datatableKeywordGenerator.generateIndexName(tableName, columnName);
return checkKeyPresent(keyNameToCheck, indexDefinitions);
}
@Override
public boolean isExplicitlyIndexed(String datatable, String columnName) {
return isExplicitlyIndexed(datatable, columnName, getDatatableIndexData(datatable));
public boolean isExplicitlyIndexed(String tableName, String columnName) {
return isExplicitlyIndexed(tableName, columnName, getDatatableIndexData(tableName));
}
private boolean checkKeyPresent(String keyNameToCheck, List<IndexDetail> indexDefinitions) {
@@ -276,25 +291,23 @@ public class GenericDataServiceImpl implements GenericDataService {
}
@SuppressWarnings("AvoidHidingCauseException")
private List<IndexDetail> getDatatableIndexData(String datatable) {
private List<IndexDetail> getDatatableIndexData(String tableName) {
try {
return databaseIndependentQueryService.getTableIndexes(dataSource, datatable);
return databaseIndependentQueryService.getTableIndexes(dataSource, tableName);
} catch (IllegalArgumentException e) {
throw new DatatableNotFoundException(datatable);
throw new DatatableNotFoundException(tableName);
}
}
/*
* Candidate for using caching there to get allowed 'column values' from code/codevalue tables
*/
private List<ResultsetColumnValueData> retrieveColumnValues(final String codeName) {
private List<ResultsetColumnValueData> retrieveCodeValues(final String codeName) {
final String sql = "select v.id, v.code_score, v.code_value from m_code m join m_code_value v on v.code_id = m.id where m.code_name = ? order by v.order_position, v.id";
final SqlRowSet rsValues = this.jdbcTemplate.queryForRowSet(sql, codeName); // NOSONAR
final List<ResultsetColumnValueData> columnValues = new ArrayList<>();
final String sql = "select v.id, v.code_score, v.code_value from m_code m join m_code_value v on v.code_id = m.id where m.code_name = ? order by v.order_position, v.id";
final SqlRowSet rsValues = this.jdbcTemplate.queryForRowSet(sql, new Object[] { codeName }); // NOSONAR
rsValues.beforeFirst();
while (rsValues.next()) {
final Integer id = rsValues.getInt("id");
@@ -308,11 +321,11 @@ public class GenericDataServiceImpl implements GenericDataService {
}
@SuppressWarnings("AvoidHidingCauseException")
private SqlRowSet getDatatableMetaData(final String datatable) {
private SqlRowSet getTableMetaData(final String tableName) {
try {
return databaseIndependentQueryService.getTableColumns(dataSource, datatable);
return databaseIndependentQueryService.getTableColumns(dataSource, tableName);
} catch (IllegalArgumentException e) {
throw new DatatableNotFoundException(datatable);
throw new DatatableNotFoundException(tableName);
}
}
}
@@ -47,6 +47,7 @@ import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.core.service.database.JdbcJavaType;
import org.apache.fineract.infrastructure.dataqueries.data.GenericResultsetData;
import org.apache.fineract.infrastructure.dataqueries.data.ReportData;
import org.apache.fineract.infrastructure.dataqueries.data.ReportParameterData;
@@ -230,7 +231,7 @@ public class ReadReportingServiceImpl implements ReadReportingService {
table.completeRow();
Integer rSize;
String currColType;
JdbcJavaType currColType;
String currVal;
log.debug("NO. of Rows: {}", data.size());
for (ResultsetRowData element : data) {
@@ -240,9 +241,7 @@ public class ReadReportingServiceImpl implements ReadReportingService {
currColType = columnHeaders.get(j).getColumnType();
currVal = (String) row.get(j);
if (currVal != null) {
if (currColType.equals("DECIMAL") || currColType.equals("DOUBLE") || currColType.equals("BIGINT")
|| currColType.equals("SMALLINT") || currColType.equals("INT")) {
if (currColType.isNumericType()) {
table.addCell(currVal.toString());
} else {
table.addCell(currVal.toString());
@@ -22,7 +22,7 @@ import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.time.LocalDate;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.interoperation.util.MathUtil;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.portfolio.note.domain.Note;
import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
@@ -30,7 +30,7 @@ import java.util.Arrays;
import java.util.List;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.interoperation.util.MathUtil;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
public class MoneyData {
@@ -48,6 +48,7 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.interoperation.data.InteropAccountData;
@@ -76,7 +77,6 @@ import org.apache.fineract.interoperation.exception.InteropTransferAlreadyCommit
import org.apache.fineract.interoperation.exception.InteropTransferAlreadyOnHoldException;
import org.apache.fineract.interoperation.exception.InteropTransferMissingException;
import org.apache.fineract.interoperation.serialization.InteropDataValidator;
import org.apache.fineract.interoperation.util.MathUtil;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepository;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
@@ -40,6 +40,7 @@ import org.apache.fineract.infrastructure.core.exception.MultiException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent;
@@ -63,7 +64,6 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.transaction
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionRecoveryPaymentPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionRecoveryPaymentPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.interoperation.util.MathUtil;
import org.apache.fineract.organisation.holiday.domain.Holiday;
import org.apache.fineract.organisation.holiday.domain.HolidayRepository;
import org.apache.fineract.organisation.holiday.domain.HolidayStatusType;
@@ -27,7 +27,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.fineract.interoperation.util.MathUtil;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidDetail;
@@ -1230,7 +1230,7 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
}
final List<DatatableData> datatableTemplates = this.entityDatatableChecksReadService
.retrieveTemplates(StatusEnum.CREATE.getCode().longValue(), EntityTables.SAVING.getName(), productId);
.retrieveTemplates(StatusEnum.CREATE.getCode().longValue(), EntityTables.SAVINGS.getName(), productId);
template.setDatatables(datatableTemplates);
return template;
@@ -1324,7 +1324,7 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
sqlBuilder.append("tr.transaction_date as transactionDate, tr.amount as transactionAmount,");
sqlBuilder.append(" tr.release_id_of_hold_amount as releaseTransactionId,");
sqlBuilder.append(" tr.reason_for_block as reasonForBlock,");
sqlBuilder.append(" tr.submitted_on_date as submittedOnDate,");
sqlBuilder.append("tr.submitted_on_date as submittedOnDate,");
sqlBuilder.append(" au.username as submittedByUsername, ");
sqlBuilder.append(" nt.note as transactionNote, ");
sqlBuilder.append("tr.running_balance_derived as runningBalance, tr.is_reversed as reversed,");
@@ -207,8 +207,8 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
final Map<String, Object> changes = account.activate(user, command, DateUtils.getBusinessLocalDate());
entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVING.getName(),
StatusEnum.ACTIVATE.getCode().longValue(), EntityTables.SAVING.getForeignKeyColumnNameOnDatatable(), account.productId());
entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVINGS.getName(),
StatusEnum.ACTIVATE.getCode().longValue(), EntityTables.SAVINGS.getForeignKeyColumnNameOnDatatable(), account.productId());
if (!changes.isEmpty()) {
final Locale locale = command.extractLocale();
@@ -924,8 +924,8 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
throw new SavingsAccountClosingNotAllowedException("linked", defaultUserMessage, savingsId);
}
entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVING.getName(),
StatusEnum.CLOSE.getCode().longValue(), EntityTables.SAVING.getForeignKeyColumnNameOnDatatable(), account.productId());
entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVINGS.getName(),
StatusEnum.CLOSE.getCode().longValue(), EntityTables.SAVINGS.getForeignKeyColumnNameOnDatatable(), account.productId());
final boolean isWithdrawBalance = command.booleanPrimitiveValueOfParameterNamed(withdrawBalanceParamName);
@@ -1517,9 +1517,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(SAVINGS_ACCOUNT_CHARGE_RESOURCE_NAME);
/***
* Only recurring fees are allowed to inactivate
*/
// Only recurring fees are allowed to inactivate
if (!savingsAccountCharge.isRecurringFee()) {
baseDataValidator.reset().parameter(null).value(savingsAccountCharge.getId())
.failWithCodeNoParameterAddedToErrorCode("charge.inactivation.allowed.only.for.recurring.charges");
@@ -1547,9 +1545,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
chargePayments.add(transaction);
}
}
/***
* Reverse the excess payments of charge transactions
*/
// Reverse the excess payments of charge transactions
SavingsAccountTransaction lastChargePayment = getLastChargePayment(chargePayments);
this.undoTransaction(savingsAccountCharge.savingsAccount().getId(), lastChargePayment.getId(), false);
updatedCharge = account.getUpdatedChargeDetails(savingsAccountCharge);
@@ -257,11 +257,12 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
final Long savingsId = account.getId();
if (command.parameterExists(SavingsApiConstants.datatables)) {
this.entityDatatableChecksWritePlatformService.saveDatatables(StatusEnum.CREATE.getCode().longValue(),
EntityTables.SAVING.getName(), savingsId, account.productId(),
EntityTables.SAVINGS.getName(), savingsId, account.productId(),
command.arrayOfParameterNamed(SavingsApiConstants.datatables));
}
this.entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVING.getName(),
StatusEnum.CREATE.getCode().longValue(), EntityTables.SAVING.getForeignKeyColumnNameOnDatatable(), account.productId());
this.entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVINGS.getName(),
StatusEnum.CREATE.getCode().longValue(), EntityTables.SAVINGS.getForeignKeyColumnNameOnDatatable(),
account.productId());
businessEventNotifierService.notifyPostBusinessEvent(new SavingsCreateBusinessEvent(account));
@@ -475,8 +476,8 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsId, false);
checkClientOrGroupActive(savingsAccount);
entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVING.getName(),
StatusEnum.APPROVE.getCode().longValue(), EntityTables.SAVING.getForeignKeyColumnNameOnDatatable(),
entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVINGS.getName(),
StatusEnum.APPROVE.getCode().longValue(), EntityTables.SAVINGS.getForeignKeyColumnNameOnDatatable(),
savingsAccount.productId());
final Map<String, Object> changes = savingsAccount.approveApplication(currentUser, command, DateUtils.getBusinessLocalDate());
@@ -598,8 +599,8 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsId, false);
checkClientOrGroupActive(savingsAccount);
entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVING.getName(),
StatusEnum.REJECTED.getCode().longValue(), EntityTables.SAVING.getForeignKeyColumnNameOnDatatable(),
entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVINGS.getName(),
StatusEnum.REJECTED.getCode().longValue(), EntityTables.SAVINGS.getForeignKeyColumnNameOnDatatable(),
savingsAccount.productId());
final Map<String, Object> changes = savingsAccount.rejectApplication(currentUser, command, DateUtils.getBusinessLocalDate());
@@ -635,8 +636,8 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsId, false);
checkClientOrGroupActive(savingsAccount);
entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVING.getName(),
StatusEnum.WITHDRAWN.getCode().longValue(), EntityTables.SAVING.getForeignKeyColumnNameOnDatatable(),
entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVINGS.getName(),
StatusEnum.WITHDRAWN.getCode().longValue(), EntityTables.SAVINGS.getForeignKeyColumnNameOnDatatable(),
savingsAccount.productId());
final Map<String, Object> changes = savingsAccount.applicantWithdrawsFromApplication(currentUser, command,
@@ -21,7 +21,11 @@ package org.apache.fineract.portfolio.search;
import java.util.HashSet;
import java.util.Set;
public class SearchConstants {
public final class SearchConstants {
public static final String API_PARAM_COLUMN = "column";
private SearchConstants() {}
public enum SearchResponseParameters {
@@ -0,0 +1,237 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.search.service;
import static java.util.Locale.ENGLISH;
import static org.apache.fineract.infrastructure.core.data.ApiParameterError.parameterErrorWithValue;
import static org.apache.fineract.infrastructure.core.service.DateUtils.DEFAULT_DATETIME_FORMAT;
import static org.apache.fineract.infrastructure.core.service.DateUtils.DEFAULT_DATE_FORMAT;
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_FIELD_MANDATORY;
import static org.apache.fineract.portfolio.search.SearchConstants.API_PARAM_COLUMN;
import com.google.common.base.Predicate;
import com.google.gson.JsonObject;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.sql.Date;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.database.DatabaseType;
import org.apache.fineract.infrastructure.core.service.database.JdbcJavaType;
import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData;
import org.apache.fineract.infrastructure.security.utils.SQLInjectionValidator;
import org.springframework.jdbc.support.rowset.SqlRowSet;
public final class SearchUtil {
public static final int DEFAULT_PAGE_SIZE = 50;
private static final JsonParserHelper helper = new JsonParserHelper();
private SearchUtil() {}
@NotNull
public static Map<String, ResultsetColumnHeaderData> mapHeadersToName(@NotNull Collection<ResultsetColumnHeaderData> columnHeaders) {
return columnHeaders.stream().collect(Collectors.toMap(ResultsetColumnHeaderData::getColumnName, e -> e));
}
public static ResultsetColumnHeaderData findFiltered(@NotNull Collection<ResultsetColumnHeaderData> columnHeaders,
@NotNull Predicate<ResultsetColumnHeaderData> filter) {
return columnHeaders.stream().filter(filter).findFirst().orElse(null);
}
public static ResultsetColumnHeaderData getFiltered(@NotNull Collection<ResultsetColumnHeaderData> columnHeaders,
@NotNull Predicate<ResultsetColumnHeaderData> filter) {
ResultsetColumnHeaderData filtered = findFiltered(columnHeaders, filter);
if (filtered == null) {
throw new PlatformDataIntegrityException("error.msg.column.not.exists", "Column filtered does not exist");
}
return filtered;
}
public static void extractJsonResult(@NotNull SqlRowSet rowSet, @NotNull List<String> selectColumns,
@NotNull List<String> resultColumns, @NotNull List<JsonObject> results) {
JsonObject json = new JsonObject();
for (int i = 0; i < selectColumns.size(); i++) {
String sCol = selectColumns.get(i).replaceAll("\"", "");
Object rowValue = rowSet.getObject(sCol);
if (rowValue != null) {
String rCol = resultColumns.get(i);
if (rowValue instanceof Character) {
json.addProperty(rCol, (Character) rowValue);
} else if (rowValue instanceof Number) {
json.addProperty(rCol, new BigDecimal(rowValue.toString()));
} else if (rowValue instanceof Boolean) {
json.addProperty(rCol, (Boolean) rowValue);
} else if (rowValue instanceof LocalDateTime) {
json.addProperty(rCol, DateUtils.format((LocalDateTime) rowValue));
} else if (rowValue instanceof Timestamp) {
json.addProperty(rCol, DateUtils.format(((Timestamp) rowValue).toLocalDateTime()));
} else if (rowValue instanceof LocalDate) {
json.addProperty(rCol, DateUtils.format((LocalDate) rowValue));
} else if (rowValue instanceof Date) {
json.addProperty(rCol, DateUtils.format(((Date) rowValue).toLocalDate()));
} else {
json.addProperty(rCol, rowValue.toString());
}
}
}
if (json.size() > 0) {
results.add(json);
}
}
public static List<String> validateToJdbcColumns(List<String> columns, Map<String, ResultsetColumnHeaderData> columnHeaders,
List<ApiParameterError> errors, boolean allowEmpty) {
List<String> result = new ArrayList<>();
if (columns == null || columns.isEmpty()) {
if (!allowEmpty) {
errors.add(parameterErrorWithValue("error.msg.columns.empty", "Columns list is empty", API_PARAM_COLUMN, null));
}
} else {
columns.forEach(rcn -> result.add(validateToJdbcColumn(rcn, columnHeaders, errors, allowEmpty)));
}
return result;
}
public static String validateToJdbcColumn(String column, Map<String, ResultsetColumnHeaderData> columnHeaders,
List<ApiParameterError> errors, boolean allowEmpty) {
if (!allowEmpty && column == null) {
errors.add(parameterErrorWithValue("error.msg.column.empty", "Column filter is empty", API_PARAM_COLUMN, null));
}
if (column != null) {
SQLInjectionValidator.validateDynamicQuery(column);
if (!columnHeaders.containsKey(column)) {
column = camelToSnake(column);
if (!columnHeaders.containsKey(column)) {
errors.add(
parameterErrorWithValue("error.msg.invalid.column", "Column not exist in database", API_PARAM_COLUMN, column));
}
}
}
return column;
}
public static Object parseAndValidateJdbcColumnValue(String column, String columnValue,
Map<String, ResultsetColumnHeaderData> columnHeaders, DatabaseType dialect) {
List<ApiParameterError> errors = new ArrayList<>();
column = validateToJdbcColumn(column, columnHeaders, errors, false);
if (!errors.isEmpty()) {
throw new PlatformApiDataValidationException(errors);
}
ResultsetColumnHeaderData columnHeader = columnHeaders.get(column);
String dateFormat = columnHeader.getColumnType().isDateTimeType() ? DEFAULT_DATETIME_FORMAT : DEFAULT_DATE_FORMAT;
return columnHeader.getColumnType().toJdbcValue(dialect,
parseAndValidateColumnValue(columnHeader, columnValue, dateFormat, ENGLISH, dialect), false);
}
public static Object parseAndValidateColumnValue(final ResultsetColumnHeaderData columnHeader, final String pValue,
final String dateFormat, final Locale locale, DatabaseType dialect) {
String columnValue = pValue;
JdbcJavaType colType = columnHeader.getColumnType();
if (!colType.isStringType() || !columnHeader.isMandatory()) {
columnValue = StringUtils.trimToNull(columnValue);
}
String errorCode = "validation.msg.validation.errors.exist";
String errorMsg = "Validation errors exist.";
if (columnValue == null && columnHeader.isMandatory()) {
ApiParameterError error = ApiParameterError.parameterError("error.msg.column.mandatory", API_FIELD_MANDATORY,
columnHeader.getColumnName());
throw new PlatformApiDataValidationException(errorCode, errorMsg, List.of(error));
}
if (StringUtils.isEmpty(columnValue)) {
return columnValue;
}
SQLInjectionValidator.validateDynamicQuery(columnValue);
if (columnHeader.hasColumnValues()) {
if (columnHeader.isCodeValueDisplayType()) {
if (!columnHeader.isColumnValueAllowed(columnValue)) {
ApiParameterError error = ApiParameterError.parameterError("error.msg.invalid.columnValue",
"Value not found in Allowed Value list", columnHeader.getColumnName(), columnValue);
throw new PlatformApiDataValidationException(errorCode, errorMsg, List.of(error));
}
return columnValue;
} else if (columnHeader.isCodeLookupDisplayType()) {
final Integer codeLookup = Integer.valueOf(columnValue);
if (!columnHeader.isColumnCodeAllowed(codeLookup)) {
ApiParameterError error = ApiParameterError.parameterError("error.msg.invalid.columnValue",
"Value not found in Allowed Value list", columnHeader.getColumnName(), columnValue);
throw new PlatformApiDataValidationException(errorCode, errorMsg, List.of(error));
}
return codeLookup;
} else {
throw new PlatformDataIntegrityException("error.msg.invalid.columnType.", "Code: " + columnHeader.getColumnName()
+ " - Invalid Type " + colType.getJdbcName(dialect) + " (neither varchar nor int)");
}
}
if (colType.isDateType()) {
return JsonParserHelper.convertFrom(columnValue, columnHeader.getColumnName(), dateFormat, locale);
}
if (colType.isDateTimeType()) {
return JsonParserHelper.convertDateTimeFrom(columnValue, columnHeader.getColumnName(), dateFormat, locale);
}
if (colType.isAnyIntegerType()) {
return helper.convertToInteger(columnValue, columnHeader.getColumnName(), locale);
}
if (colType.isDecimalType()) {
return helper.convertFrom(columnValue, columnHeader.getColumnName(), locale);
}
if (colType.isBooleanType()) {
final Boolean boolValue = BooleanUtils.toBooleanObject(columnValue);
if (boolValue == null) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.invalid.boolean.format",
"The parameter " + columnHeader.getColumnName() + " has value: " + columnValue + " which is invalid boolean value.",
columnHeader.getColumnName(), columnValue);
throw new PlatformApiDataValidationException(errorCode, errorMsg, List.of(error));
}
return boolValue;
}
if (colType.isStringType()) {
if (columnHeader.getColumnLength() > 0 && columnValue.length() > columnHeader.getColumnLength()) {
ApiParameterError error = ApiParameterError.parameterError("validation.msg.datatable.entry.column.exceeds.maxlength",
"The column `" + columnHeader.getColumnName() + "` exceeds its defined max-length ", columnHeader.getColumnName(),
columnValue);
throw new PlatformApiDataValidationException(errorCode, errorMsg, List.of(error));
}
}
return columnValue;
}
public static String camelToSnake(final String camelStr) {
return camelStr == null ? null
: camelStr.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2").replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase();
}
}
@@ -22,8 +22,10 @@ import static java.util.Collections.emptyList;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.when;
import com.google.gson.JsonObject;
@@ -31,6 +33,7 @@ import java.util.List;
import java.util.stream.Stream;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.core.service.database.DatabaseType;
import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver;
import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData;
import org.apache.fineract.infrastructure.dataqueries.exception.DatatableNotFoundException;
@@ -71,6 +74,7 @@ public class ReadWriteNonCoreDataServiceImplTest {
@Test
public void testSqlInjectionCaughtQueryDataTable() {
mockDatatableValidation();
assertThrows(SQLInjectionException.class, () -> {
underTest.queryDataTable("table", "cf1", "vf1", "' or 1=1");
});
@@ -78,6 +82,7 @@ public class ReadWriteNonCoreDataServiceImplTest {
@Test
public void testSqlInjectionCaughtQueryDataTable2() {
mockDatatableValidation();
assertThrows(SQLInjectionException.class, () -> {
underTest.queryDataTable("table", "cf1", "vf1", "1; DROP TABLE m_loan; SELECT");
});
@@ -87,18 +92,24 @@ public class ReadWriteNonCoreDataServiceImplTest {
public void testQueryDataTableSuccess() {
mockDatatableValidation();
SqlRowSet sqlRS = Mockito.mock(SqlRowSet.class);
when(jdbcTemplate.queryForRowSet(eq("select rc1,rc2 from table where cf1 = ?"), any(Object.class))).thenReturn(sqlRS);
when(jdbcTemplate.queryForRowSet(eq("SELECT \"rc1\", \"rc2\" FROM \"table\" WHERE \"cf1\" = ?"), any(Object.class)))
.thenReturn(sqlRS);
when(sqlRS.next()).thenReturn(true).thenReturn(false);
when(sqlRS.getObject(anyString())).thenReturn("value1").thenReturn("value2");
when(sqlGenerator.escape(anyString())).thenReturn("rc1").thenReturn("rc2").thenReturn("table").thenReturn("cf1");
when(sqlGenerator.buildSelect(anyCollection(), nullable(String.class), eq(false))).thenReturn("SELECT \"rc1\", \"rc2\"");
when(sqlGenerator.buildFrom(anyString(), nullable(String.class), eq(false))).thenReturn("FROM \"table\"");
when(sqlGenerator.escape(anyString())).thenReturn("\"cf1\"");
when(sqlGenerator.alias(anyString(), nullable(String.class))).thenReturn("\"cf1\"");
when(databaseTypeResolver.isPostgreSQL()).thenReturn(true);
when(databaseTypeResolver.databaseType()).thenReturn(DatabaseType.POSTGRESQL);
DatabaseType dialect = databaseTypeResolver.databaseType();
ResultsetColumnHeaderData cf1 = ResultsetColumnHeaderData.detailed("cf1", "text", 10L, false, false, emptyList(), null, false,
false);
false, dialect);
ResultsetColumnHeaderData rc1 = ResultsetColumnHeaderData.detailed("rc1", "text", 10L, false, false, emptyList(), null, false,
false);
false, dialect);
ResultsetColumnHeaderData rc2 = ResultsetColumnHeaderData.detailed("rc2", "text", 10L, false, false, emptyList(), null, false,
false);
false, dialect);
when(genericDataService.fillResultsetColumnHeaders("table")).thenReturn(List.of(cf1, rc1, rc2));
List<JsonObject> results = underTest.queryDataTable("table", "cf1", "vf1", "rc1,rc2");
@@ -125,12 +136,14 @@ public class ReadWriteNonCoreDataServiceImplTest {
@MethodSource("provideParameters")
public void testQueryDataTableInvalidParameterError(String columnType, String errorMessage) {
mockDatatableValidation();
when(databaseTypeResolver.databaseType()).thenReturn(DatabaseType.POSTGRESQL);
DatabaseType dialect = databaseTypeResolver.databaseType();
ResultsetColumnHeaderData cf1 = ResultsetColumnHeaderData.detailed("cf1", columnType, 10L, false, false, emptyList(), null, false,
false);
false, dialect);
ResultsetColumnHeaderData rc1 = ResultsetColumnHeaderData.detailed("rc1", "text", 10L, false, false, emptyList(), null, false,
false);
false, dialect);
ResultsetColumnHeaderData rc2 = ResultsetColumnHeaderData.detailed("rc2", "text", 10L, false, false, emptyList(), null, false,
false);
false, dialect);
when(genericDataService.fillResultsetColumnHeaders("table")).thenReturn(List.of(cf1, rc1, rc2));
PlatformApiDataValidationException thrown = assertThrows(PlatformApiDataValidationException.class,