diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/ApiParameterError.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/ApiParameterError.java index fa140d1ee..3a50cfeac 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/ApiParameterError.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/ApiParameterError.java @@ -69,16 +69,12 @@ public final class ApiParameterError { public static ApiParameterError parameterError(final String globalisationMessageCode, final String defaultUserMessage, final String parameterName, final Object... defaultUserMessageArgs) { - final ApiParameterError error = new ApiParameterError(globalisationMessageCode, defaultUserMessage, defaultUserMessageArgs, - parameterName, null); - return error; + return new ApiParameterError(globalisationMessageCode, defaultUserMessage, defaultUserMessageArgs, parameterName, null); } public static ApiParameterError parameterErrorWithValue(final String globalisationMessageCode, final String defaultUserMessage, final String parameterName, final String value, final Object... defaultUserMessageArgs) { - final ApiParameterError error = new ApiParameterError(globalisationMessageCode, defaultUserMessage, defaultUserMessageArgs, - parameterName, value); - return error; + return new ApiParameterError(globalisationMessageCode, defaultUserMessage, defaultUserMessageArgs, parameterName, value); } private ApiParameterError(final String globalisationMessageCode, final String defaultUserMessage, final Object[] defaultUserMessageArgs, diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java index c1f0a0d29..159e54c17 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java @@ -25,18 +25,21 @@ import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DateTimeParseException; import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Locale; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.jetbrains.annotations.NotNull; public final class DateUtils { public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; public static final String DEFAULT_DATETIME_FORMAT = DEFAULT_DATE_FORMAT + " HH:mm:ss"; - public static final DateTimeFormatter DEFAULT_DATETIME_FORMATTER = new DateTimeFormatterBuilder().appendPattern(DEFAULT_DATETIME_FORMAT) - .toFormatter(); - public static final DateTimeFormatter DEFAULT_DATE_FORMATTER = new DateTimeFormatterBuilder().appendPattern(DEFAULT_DATE_FORMAT) - .toFormatter(); + public static final DateTimeFormatter DEFAULT_DATE_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT); + public static final DateTimeFormatter DEFAULT_DATETIME_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_DATETIME_FORMAT); private DateUtils() { @@ -86,4 +89,106 @@ public final class DateUtils { public static long getDifferenceInDays(final LocalDate localDateBefore, final LocalDate localDateAfter) { return DAYS.between(localDateBefore, localDateAfter); } + + public static int compareToBusinessDate(LocalDate date) { + return compare(date, getBusinessLocalDate()); + } + + public static boolean isEqualBusinessDate(LocalDate date) { + return isEqual(date, getBusinessLocalDate()); + } + + public static boolean isBeforeBusinessDate(LocalDate date) { + return isBefore(date, getBusinessLocalDate()); + } + + public static boolean isAfterBusinessDate(LocalDate date) { + return isAfter(date, getBusinessLocalDate()); + } + + public static int compare(LocalDate first, LocalDate second) { + return first == null ? (second == null ? 0 : -1) : first.compareTo(second); + } + + public static boolean isEqual(LocalDate first, LocalDate second) { + return first == null ? second == null : (second != null && first.isEqual(second)); + } + + public static boolean isBefore(LocalDate first, LocalDate second) { + return second != null && (first == null || first.isBefore(second)); + } + + public static boolean isAfter(LocalDate first, LocalDate second) { + return first != null && (second == null || first.isAfter(second)); + } + + public static LocalDate parseLocalDate(String stringDate) { + return parseLocalDate(stringDate, null); + } + + public static LocalDate parseLocalDate(String stringDate, String format) { + return parseLocalDate(stringDate, format, null); + } + + public static LocalDate parseLocalDate(String stringDate, String format, Locale locale) { + if (stringDate == null) { + return null; + } + DateTimeFormatter formatter = getDateFormatter(format, locale); + try { + return LocalDate.parse(stringDate, formatter); + } catch (final DateTimeParseException e) { + final List errors = List.of(ApiParameterError.parameterError("validation.msg.invalid.date.pattern", + "The parameter date (" + stringDate + ") format is invalid", "date", stringDate)); + throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors, e); + } + } + + public static String format(LocalDate date) { + return format(date, null); + } + + public static String format(LocalDate date, String format) { + return format(date, format, null); + } + + public static String format(LocalDate date, String format, Locale locale) { + return date == null ? null : date.format(getDateFormatter(format, locale)); + } + + public static String format(LocalDateTime dateTime) { + return format(dateTime, null); + } + + public static String format(LocalDateTime dateTime, String format) { + return format(dateTime, format, null); + } + + public static String format(LocalDateTime dateTime, String format, Locale locale) { + return dateTime == null ? null : dateTime.format(getDateTimeFormatter(format, locale)); + } + + @NotNull + private static DateTimeFormatter getDateFormatter(String format, Locale locale) { + DateTimeFormatter formatter = DEFAULT_DATE_FORMATTER; + if (format != null || locale != null) { + if (format == null) { + format = DEFAULT_DATE_FORMAT; + } + formatter = locale == null ? DateTimeFormatter.ofPattern(format) : DateTimeFormatter.ofPattern(format, locale); + } + return formatter; + } + + @NotNull + private static DateTimeFormatter getDateTimeFormatter(String format, Locale locale) { + DateTimeFormatter formatter = DEFAULT_DATETIME_FORMATTER; + if (format != null || locale != null) { + if (format == null) { + format = DEFAULT_DATETIME_FORMAT; + } + formatter = locale == null ? DateTimeFormatter.ofPattern(format) : DateTimeFormatter.ofPattern(format, locale); + } + return formatter; + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/interoperation/util/MathUtil.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java similarity index 98% rename from fineract-loan/src/main/java/org/apache/fineract/interoperation/util/MathUtil.java rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java index f1550b26b..285183bb3 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/interoperation/util/MathUtil.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.interoperation.util; +package org.apache.fineract.infrastructure.core.service; import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; @@ -31,6 +31,10 @@ public final class MathUtil { } + public static E nullToDefault(E value, E def) { + return value == null ? def : value; + } + public static Long nullToZero(Long value) { return nullToDefault(value, 0L); } @@ -328,6 +332,10 @@ public final class MathUtil { return isEmpty(amount) ? amount : amount.negate(mc); } + public static String formatToSql(BigDecimal amount) { + return amount == null ? null : amount.toPlainString(); + } + // ----------------- Money ----------------- public static Money nullToZero(Money value, @NotNull MonetaryCurrency currency) { diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGenerator.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGenerator.java index fc9591992..cce2807b9 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGenerator.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGenerator.java @@ -20,8 +20,14 @@ package org.apache.fineract.infrastructure.core.service.database; import static java.lang.String.format; +import jakarta.validation.constraints.NotNull; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.logging.log4j.util.Strings; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; @Component @@ -36,14 +42,19 @@ public class DatabaseSpecificSQLGenerator { } public String escape(String arg) { - if (databaseTypeResolver.isMySQL()) { + DatabaseType dialect = databaseTypeResolver.databaseType(); + if (dialect.isMySql()) { return format("`%s`", arg); - } else if (databaseTypeResolver.isPostgreSQL()) { + } else if (dialect.isPostgres()) { return format("\"%s\"", arg); } return arg; } + public String formatValue(JdbcJavaType columnType, String value) { + return (columnType.isStringType() || columnType.isAnyDateType()) ? format("'%s'", value) : value; + } + public String groupConcat(String arg) { if (databaseTypeResolver.isMySQL()) { return format("GROUP_CONCAT(%s)", arg); @@ -189,4 +200,49 @@ public class DatabaseSpecificSQLGenerator { throw new IllegalStateException("Database type is not supported for casting to json " + databaseTypeResolver.databaseType()); } } + + public String alias(@NotNull String field, String alias) { + return Strings.isEmpty(alias) ? field : (alias + '.') + field; + } + + public String buildSelect(Collection fields, String alias, boolean embedded) { + if (fields == null || fields.isEmpty()) { + return ""; + } + String select = ""; + if (!embedded) { + select = "SELECT "; + } + return select + fields.stream().map(e -> alias(escape(e), alias)).collect(Collectors.joining(", ")); + } + + public String buildFrom(String definition, String alias, boolean embedded) { + if (definition == null) { + return ""; + } + String from = ""; + if (!embedded) { + from = "FROM "; + } + return from + escape(definition) + (Strings.isEmpty(alias) ? "" : (" " + alias)); + } + + public String buildJoin(@NotNull String definition, String alias, @NotNull String fkCol, String refAlias, @NotNull String refCol, + String joinType) { + String join = Strings.isEmpty(joinType) ? "JOIN" : (joinType + " JOIN"); + alias = Strings.isEmpty(alias) ? "" : (" " + alias); + return format("%s %s%s ON %s = %s", join, escape(definition), alias, alias(escape(fkCol), alias), alias(escape(refCol), refAlias)); + } + + public String buildOrderBy(List orders, String alias, boolean embedded) { + if (orders == null || orders.isEmpty()) { + return ""; + } + String orderBy = ""; + if (!embedded) { + orderBy = "ORDER BY "; + } + return orderBy + orders.stream().map(e -> String.join(" ", alias(escape(e.getProperty()), alias), e.getDirection().name())) + .collect(Collectors.joining(", ")); + } } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseType.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseType.java index 6b3954d33..269550f1c 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseType.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseType.java @@ -19,5 +19,16 @@ package org.apache.fineract.infrastructure.core.service.database; public enum DatabaseType { - MYSQL, POSTGRESQL + + MYSQL, // + POSTGRESQL, // + ; + + public boolean isMySql() { + return this == MYSQL; + } + + public boolean isPostgres() { + return this == POSTGRESQL; + } } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseTypeResolver.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseTypeResolver.java index d41ce9031..4bdc87391 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseTypeResolver.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseTypeResolver.java @@ -58,10 +58,10 @@ public class DatabaseTypeResolver implements InitializingBean { } public boolean isPostgreSQL() { - return DatabaseType.POSTGRESQL.equals(currentDatabaseType.get()); + return DatabaseType.POSTGRESQL == currentDatabaseType.get(); } public boolean isMySQL() { - return DatabaseType.MYSQL.equals(currentDatabaseType.get()); + return DatabaseType.MYSQL == currentDatabaseType.get(); } } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/JavaType.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/JavaType.java new file mode 100644 index 000000000..56b31d236 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/JavaType.java @@ -0,0 +1,371 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.service.database; + +import jakarta.validation.constraints.NotNull; +import java.io.InputStream; +import java.io.Reader; +import java.io.Serializable; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Time; +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.springframework.data.domain.Persistable; + +public enum JavaType { + + BOOLEAN(boolean.class), // + BYTE(byte.class), // + SHORT(short.class), // + INT(int.class), // + LONG(long.class), // + CHAR(char.class), // + FLOAT(float.class), // + DOUBLE(double.class), // + // non-primitive types + BOOLEAN_OBJ(Boolean.class), // + BYTE_OBJ(Byte.class), // + SHORT_OBJ(Short.class), // + INT_OBJ(Integer.class), // + LONG_OBJ(Long.class), // + CHAR_OBJ(Character.class), // + FLOAT_OBJ(Float.class), // + DOUBLE_OBJ(Double.class), // + BIGDECIMAL(BigDecimal.class), // + BIGINTEGER(BigInteger.class), // + NUMBER(Number.class), // + STRING(String.class), // + DATETIME(Timestamp.class), // + TIME(Time.class), // + DATE(Date.class), // + // Java8 time API + LOCAL_DATE(LocalDate.class), // + LOCAL_TIME(LocalTime.class), // + LOCAL_DATETIME(LocalDateTime.class), // + OFFSET_TIME(OffsetTime.class), // + OFFSET_DATETIME(OffsetDateTime.class), // + + BINARY(byte[].class), // + ARRAY(null), // + COLLECTION(Collection.class), // + MAP(Map.class), // + PC(null), // + LOCALE(Locale.class), // + PC_UNTYPED(Persistable.class), // + CALENDAR(Calendar.class), // + OID(null), // + INPUT_STREAM(InputStream.class), // + INPUT_READER(Reader.class), // + ENUM(Enum.class), // + OBJECT(Object.class), // + ; + + public static final JavaType[] VALUES = values(); + + private static final Map, JavaType> BY_TYPE = Arrays.stream(VALUES).filter(e -> e.type != null) + .collect(Collectors.toMap(JavaType::getTypeClass, v -> v)); + + private final Class type; + + JavaType(Class type) { + this.type = type; + } + + public Class getTypeClass() { + return type; + } + + public boolean isPrimitive() { + return ordinal() <= DOUBLE.ordinal(); + } + + public boolean isBooleanType() { + return this == BOOLEAN || this == BOOLEAN_OBJ; + } + + public boolean isByteType() { + return this == BYTE || this == BYTE_OBJ; + } + + public boolean isShortType() { + return this == SHORT || this == SHORT_OBJ; + } + + public boolean isIntegerType() { + return this == INT || this == INT_OBJ; + } + + public boolean isLongType() { + return this == LONG || this == LONG_OBJ; + } + + public boolean isAnyIntegerType() { + return isByteType() || isShortType() || isIntegerType() || isLongType() || this == BIGINTEGER; + } + + public boolean isFloatType() { + return this == FLOAT || this == FLOAT_OBJ; + } + + public boolean isDoubleType() { + return this == DOUBLE || this == DOUBLE_OBJ; + } + + public boolean isAnyFloatType() { + return isFloatType() || isDoubleType(); + } + + public boolean isDecimalType() { + return this == BIGDECIMAL; + } + + public boolean isObjectNumericType() { + return Number.class.isAssignableFrom(getTypeClass()); + } + + public boolean isPrimitiveNumericType() { + return isPrimitive() && this != BOOLEAN && this != CHAR; + } + + public boolean isNumericType() { + return isPrimitiveNumericType() || isObjectNumericType(); + } + + public boolean isStringType() { + return this == STRING; + } + + public boolean isCharacterType() { + return this == CHAR || this == CHAR_OBJ; + } + + public boolean isDateType() { + return this == DATE || this == LOCAL_DATE; + } + + public boolean isTimeType() { + return this == TIME || this == LOCAL_TIME || this == OFFSET_TIME; + } + + public boolean isDateTimeType() { + return this == DATETIME || this == LOCAL_DATETIME || this == OFFSET_DATETIME; + } + + public boolean isAnyDateType() { + return isDateType() || isTimeType() || isDateTimeType(); + } + + public boolean isBinaryType() { + return this == BINARY; + } + + public JavaType getPrimitiveType() { + if (isPrimitive()) { + return this; + } + return switch (this) { + case BOOLEAN_OBJ -> BOOLEAN; + case BYTE_OBJ -> BYTE; + case SHORT_OBJ -> SHORT; + case INT_OBJ -> INT; + case LONG_OBJ -> LONG; + case CHAR_OBJ -> CHAR; + case DOUBLE_OBJ -> DOUBLE; + case FLOAT_OBJ -> FLOAT; + default -> null; + }; + } + + @NotNull + public JavaType getObjectType() { + if (!isPrimitive()) { + return this; + } + return switch (this) { + case BOOLEAN -> BOOLEAN_OBJ; + case BYTE -> BYTE_OBJ; + case SHORT -> SHORT_OBJ; + case INT -> INT_OBJ; + case LONG -> LONG_OBJ; + case CHAR -> CHAR_OBJ; + case DOUBLE -> DOUBLE_OBJ; + case FLOAT -> FLOAT_OBJ; + default -> + throw new PlatformServiceUnavailableException("error.msg.database.type.not.allowed", "Unknown primitive type " + this); + }; + } + + /** + * @return the field metadata type for the given class. First class objects are not recognized in this method. + */ + @NotNull + public static JavaType forType(Class type) { + if (type == null) { + return OBJECT; + } + JavaType javaType = BY_TYPE.get(type); + if (javaType != null) { + return javaType; + } + if (type.isArray()) { + return ARRAY; + } + // have to do this first to catch custom collection and map types; + // on resolve we figure out if these custom types are + // persistence-capable + for (Class aType : BY_TYPE.keySet()) { + if (aType.isAssignableFrom(type)) { + return BY_TYPE.get(aType); + } + } + + if (type.isInterface()) { + return Serializable.class.isAssignableFrom(type) ? OBJECT : PC_UNTYPED; + } + if (type.isAssignableFrom(Reader.class)) { + return INPUT_READER; + } + if (type.isAssignableFrom(InputStream.class)) { + return INPUT_STREAM; + } + return OBJECT; + } + + /** + * @return the field metadata type for the given class. First class objects are not recognized in this method. + */ + public boolean matchType(Class type) { + return matchType(type, true, false); + } + + /** + * @return the field metadata type for the given class. First class objects are not recognized in this method. + */ + public boolean matchType(Class type, boolean strict) { + return matchType(type, strict, false); + } + + private boolean matchType(Class type, boolean strict, boolean embedded) { + if (type == null) { + return this == OBJECT; + } + if (this.type == null) { + return false; + } + if (type.equals(this.type)) { + return true; + } + if (type.isArray()) { + return this.type.isArray(); + } + if (this.type.isAssignableFrom(type)) { + return true; + } + if (type.isInterface()) { + return Serializable.class.isAssignableFrom(type) ? this == OBJECT : this == PC_UNTYPED; + } + if (type.isAssignableFrom(Reader.class)) { + return this == INPUT_READER; + } + if (type.isAssignableFrom(InputStream.class)) { + return this == INPUT_STREAM; + } + if (!strict && !embedded) { + JavaType thisType = this; + if (isPrimitive() && !type.isPrimitive()) { + thisType = getObjectType(); + } else if (type.isPrimitive()) { + JavaType primitiveType = getPrimitiveType(); + if (primitiveType != null) { + thisType = primitiveType; + } + } + if (this != thisType && thisType.matchType(type, false, true)) { + return true; + } + if (Number.class.isAssignableFrom(type) && isNumericType()) { + JavaType thatType = forType(type); + if (thisType.isAnyIntegerType() && thatType.isAnyIntegerType()) { + return thatType.ordinal() <= thisType.ordinal(); + } + if (thisType.isAnyFloatType() && thatType.isAnyFloatType()) { + return thatType.ordinal() <= thisType.ordinal(); + } + } + } + return this == OBJECT; + } + + /** + * Determine whether the provided Object value is the default for this java type. For example: If o = Integer(0) and + * typeCode = JavaTypes.INT, this method will return true. + */ + public boolean isDefault(Object o) { + return switch (this) { + case BOOLEAN, BOOLEAN_OBJ -> Boolean.FALSE.equals(o); + case BYTE, BYTE_OBJ -> o != null && ((Byte) o) == 0; + case SHORT, SHORT_OBJ -> o != null && ((Short) o) == 0; + case INT, INT_OBJ -> o != null && ((Integer) o) == 0; + case LONG, LONG_OBJ -> o != null && ((Long) o) == 0L; + case CHAR, CHAR_OBJ -> o != null && ((Character) o) == '\u0000'; + case FLOAT, FLOAT_OBJ -> o != null && ((Float) o) == 0.0F; + case DOUBLE, DOUBLE_OBJ -> o != null && ((Double) o) == 0.0d; + default -> false; + }; + } + + public Object parse(String s, String format, Locale locale) { + if (s == null) { + return null; + } + return switch (this) { + case BOOLEAN, BOOLEAN_OBJ -> Boolean.valueOf(s); + case BYTE, BYTE_OBJ -> Byte.parseByte(s); + case SHORT, SHORT_OBJ -> Short.parseShort(s); + case INT, INT_OBJ -> Integer.parseInt(s); + case LONG, LONG_OBJ -> Long.parseLong(s); + case CHAR, CHAR_OBJ -> StringUtils.isEmpty(s) ? null : s.charAt(0); + case FLOAT, FLOAT_OBJ -> Float.parseFloat(s); + case DOUBLE, DOUBLE_OBJ -> Double.parseDouble(s); + case BIGDECIMAL -> new BigDecimal(s, MoneyHelper.getMathContext()); + case BIGINTEGER -> new BigInteger(s); + case STRING -> s; + case DATETIME -> Timestamp.valueOf(s); + case TIME -> Time.valueOf(s); + case DATE -> new Date(s); + default -> throw new PlatformServiceUnavailableException("error.msg.database.type.parse", + "Parse string representation is not supported " + this); + }; + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/JdbcJavaType.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/JdbcJavaType.java new file mode 100644 index 000000000..fc43db9a1 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/JdbcJavaType.java @@ -0,0 +1,342 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.service.database; + +import com.google.common.collect.ImmutableList; +import jakarta.validation.constraints.NotNull; +import java.sql.JDBCType; +import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException; + +public enum JdbcJavaType { + + // Mysql, PostgreSql + BIT(JavaType.BOOLEAN, new DialectType(JDBCType.BIT), new DialectType(JDBCType.BIT, true)) { // + + @Override + public Object toJdbcValueImpl(@NotNull DatabaseType dialect, Object value) { + return Boolean.TRUE.equals(value) ? 1 : 0; + } + }, + BOOLEAN(JavaType.BOOLEAN, new DialectType(JDBCType.BIT), new DialectType(JDBCType.BOOLEAN, null, "BOOL")) { // + + @Override + public Object toJdbcValueImpl(@NotNull DatabaseType dialect, Object value) { + return dialect.isMySql() ? (Boolean.TRUE.equals(value) ? 1 : 0) : super.toJdbcValueImpl(dialect, value); + } + }, + SMALLINT(JavaType.SHORT, new DialectType(JDBCType.SMALLINT, true), new DialectType(JDBCType.SMALLINT, null, "INT2")), // + TINYINT(JavaType.SHORT, new DialectType(JDBCType.TINYINT, true), new DialectType(JDBCType.SMALLINT)) { // + + @Override + public Object toJdbcValueImpl(@NotNull DatabaseType dialect, Object value) { + return dialect.isMySql() && value instanceof Boolean ? (Boolean.TRUE.equals(value) ? 1 : 0) + : super.toJdbcValueImpl(dialect, value); + } + }, + INTEGER(JavaType.INT, new DialectType(JDBCType.INTEGER, null, true, "INT"), new DialectType(JDBCType.INTEGER, null, "INT", "INT4")), // + MEDIUMINT(JavaType.INT, new DialectType(JDBCType.INTEGER, "MEDIUMINT", true), new DialectType(JDBCType.INTEGER)), // + BIGINT(JavaType.LONG, new DialectType(JDBCType.BIGINT, true), new DialectType(JDBCType.BIGINT, null, "INT8")), // + REAL(JavaType.FLOAT, new DialectType(JDBCType.FLOAT, true, true), new DialectType(JDBCType.REAL)), // + FLOAT(JavaType.FLOAT, new DialectType(JDBCType.FLOAT, true, true), new DialectType(JDBCType.REAL, null, "FLOAT4")), // + DOUBLE(JavaType.DOUBLE, new DialectType(JDBCType.DOUBLE, null, true, true, "DOUBLE PRECISION", "REAL"), + new DialectType(JDBCType.DOUBLE, "DOUBLE PRECISION", "FLOAT8")), // + NUMERIC(JavaType.BIGDECIMAL, new DialectType(JDBCType.NUMERIC, true, true), new DialectType(JDBCType.NUMERIC, true, true)), // + DECIMAL(JavaType.BIGDECIMAL, new DialectType(JDBCType.DECIMAL, "DECIMAL", true, true, "DEC", "FIXED"), + new DialectType(JDBCType.DECIMAL, true, true)), // + SERIAL(JavaType.INT, new DialectType(JDBCType.INTEGER, "{type} AUTO_INCREMENT", true, false), + new DialectType(JDBCType.INTEGER, "SERIAL", "SERIAL4")), // + SMALLSERIAL(JavaType.SHORT, new DialectType(JDBCType.SMALLINT, "{type} AUTO_INCREMENT", true, false), + new DialectType(JDBCType.SMALLINT, "SMALLSERIAL")), // + BIGSERIAL(JavaType.LONG, new DialectType(JDBCType.BIGINT, "{type} AUTO_INCREMENT", true), + new DialectType(JDBCType.BIGINT, "BIGSERIAL", "SERIAL8")), // + MONEY(JavaType.BIGDECIMAL, new DialectType(JDBCType.DECIMAL, "DECIMAL(19,2)"), new DialectType(JDBCType.DECIMAL, "MONEY")), // + CHAR(JavaType.STRING, new DialectType(JDBCType.CHAR, true), new DialectType(JDBCType.CHAR, null, true, "CHARACTER", "BPCHAR")), // + VARCHAR(JavaType.STRING, new DialectType(JDBCType.VARCHAR, true), new DialectType(JDBCType.VARCHAR, null, true, "CHARACTER VARYING")), // + LONGVARCHAR(JavaType.STRING, new DialectType(JDBCType.VARCHAR, true), new DialectType(JDBCType.VARCHAR, true)), // + TEXT(JavaType.STRING, new DialectType(JDBCType.VARCHAR, "TEXT"), new DialectType(JDBCType.VARCHAR, "TEXT")), // + TINYTEXT(JavaType.STRING, new DialectType(JDBCType.VARCHAR, "TINYTEXT"), new DialectType(JDBCType.VARCHAR, "TEXT")), // + MEDIUMTEXT(JavaType.STRING, new DialectType(JDBCType.VARCHAR, "MEDIUMTEXT"), new DialectType(JDBCType.VARCHAR, "TEXT")), // + LONGTEXT(JavaType.STRING, new DialectType(JDBCType.VARCHAR, "LONGTEXT"), new DialectType(JDBCType.VARCHAR, "TEXT")), // + DATE(JavaType.LOCAL_DATE, new DialectType(JDBCType.DATE), new DialectType(JDBCType.DATE)), // + // precision for TIME, TIMESTAMP (postgres) and INTERVAL specifies the number of fractional digits retained in the + // seconds field, but by default, there is no explicit bound on precision + TIME(JavaType.LOCAL_TIME, new DialectType(JDBCType.TIME), new DialectType(JDBCType.TIME, null, "TIME WITHOUT TIME ZONE")), // + TIME_WITH_TIMEZONE(JavaType.OFFSET_TIME, new DialectType(JDBCType.TIME_WITH_TIMEZONE, "TIME"), + new DialectType(JDBCType.TIME_WITH_TIMEZONE, null, "TIME WITH TIME ZONE")), // + DATETIME(JavaType.LOCAL_DATETIME, new DialectType(JDBCType.TIMESTAMP, "DATETIME", "TIMESTAMP"), + new DialectType(JDBCType.TIMESTAMP, null, "TIMESTAMP WITHOUT TIME ZONE")), // + TIMESTAMP_WITH_TIMEZONE(JavaType.OFFSET_DATETIME, new DialectType(JDBCType.TIMESTAMP_WITH_TIMEZONE, null, "DATETIME"), + new DialectType(JDBCType.TIMESTAMP_WITH_TIMEZONE, null, "TIMESTAMP WITH TIME ZONE", "TIMESTAMPTZ")), // + INTERVAL(JavaType.TIME, new DialectType(JDBCType.TIME), new DialectType(JDBCType.TIME, "INTERVAL")), // + BINARY(JavaType.BINARY, new DialectType(JDBCType.BINARY, true), new DialectType(JDBCType.BINARY, "BYTEA")), // + VARBINARY(JavaType.BINARY, new DialectType(JDBCType.VARBINARY, true), new DialectType(JDBCType.VARBINARY, "BYTEA")), // + LONGVARBINARY(JavaType.BINARY, new DialectType(JDBCType.VARBINARY, true), new DialectType(JDBCType.VARBINARY, "BYTEA")), // + BYTEA(JavaType.BINARY, new DialectType(JDBCType.BLOB, "LONGBLOB"), new DialectType(JDBCType.BLOB, "BYTEA")), // + BLOB(JavaType.BINARY, new DialectType(JDBCType.BLOB, "BLOB"), new DialectType(JDBCType.BLOB, "BYTEA")), // + TINYBLOB(JavaType.BINARY, new DialectType(JDBCType.BLOB, "TINYBLOB"), new DialectType(JDBCType.BLOB, "BYTEA")), // + MEDIUMBLOB(JavaType.BINARY, new DialectType(JDBCType.BLOB, "MEDIUMBLOB"), new DialectType(JDBCType.BLOB, "BYTEA")), // + LONGBLOB(JavaType.BINARY, new DialectType(JDBCType.BLOB, "LONGBLOB"), new DialectType(JDBCType.BLOB, "BYTEA")), // + ; + + // /* Not used types */ + // /* *** java.sql.JDBCType *** */ + // NULL, OTHER, JAVA_OBJECT, DISTINCT, STRUCT, ARRAY, CLOB, REF, DATALINK + // /* JDBC 4.0 Types */ + // ROWID, NCHAR(Types.NCHAR), NVARCHAR, NCLOB, SQLXML + // /* JDBC 4.2 Types */ + // REF_CURSOR, TIME_WITH_TIMEZONE, TIMESTAMP_WITH_TIMEZONE + // /* *** POSTGRES *** */ + // bit varying [(n)] / varbit, box, cidr, circle, inet, interval, line, lseg, macaddr, path, point, polygon, time + // [(p)] with time zone, timez, + // timestamp [(p)] with time zone, timestamptz, bit(n), bit varying(n), array[n] + // /* *** MYSQL *** */ + // year, enum, set, spatial types, JSON + + private static final String PLACEHOLDER_TYPE = "{type}"; + + @NotNull + private final JavaType javaType; + private final ImmutableList dialectTypes; + + JdbcJavaType(@NotNull JavaType javaType, DialectType... dialectTypes) { + this.javaType = javaType; + this.dialectTypes = ImmutableList.copyOf(dialectTypes); + } + + @NotNull + public JavaType getJavaType() { + return javaType; + } + + public static JdbcJavaType getByTypeName(@NotNull DatabaseType dialect, String name) { + if (name == null) { + return null; + } + name = name.toUpperCase(); + + for (JdbcJavaType type : values()) { + DialectType dialectType = type.getDialectType(dialect); + if (dialectType.getNameResolved().equals(name)) { + return type; + } + if (dialectType.alterNames != null) { + for (String alterName : dialectType.alterNames) { + if (alterName.equals(name)) { + return type; + } + } + } + } + return null; + } + + @NotNull + private DialectType getDialectType(@NotNull DatabaseType dialect) { + DialectType dialectType = dialectTypes.get(dialect.ordinal()); + if (dialectType == null) { + throw new PlatformServiceUnavailableException("error.msg.database.dialect.not.allowed", + "Dialect " + dialect + " is not supported for " + this); + } + return dialectType; + } + + public boolean isBooleanType() { + return getJavaType().isBooleanType(); + } + + public boolean canBooleanType(@NotNull DatabaseType dialect) { + return isBooleanType() || (dialect.isMySql() && this == TINYINT); + } + + public boolean isStringType() { + // ("char", "varchar", "blob", "text", "tinyblob", "tinytext", "mediumblob", "mediumtext", "longblob", + // "longtext") + return getJavaType().isStringType(); + } + + public boolean isVarcharType() { + // ("char", "varchar", "blob", "text", "tinyblob", "tinytext", "mediumblob", "mediumtext", "longblob", + // "longtext") + return this == VARCHAR || this == LONGVARCHAR; + } + + public boolean isTextType() { + return this == TEXT || this == TINYTEXT || this == MEDIUMTEXT || this == LONGTEXT; + } + + public boolean isSerialType() { + return this == SERIAL || this == SMALLSERIAL || this == BIGSERIAL; + } + + public boolean isIntegerType() { + return getJavaType().isIntegerType(); + } + + public boolean isAnyIntegerType() { + return getJavaType().isAnyIntegerType(); + } + + public boolean isFloatType() { + return getJavaType().isFloatType(); + } + + public boolean isAnyFloatType() { + return getJavaType().isAnyFloatType(); + } + + public boolean isDecimalType() { + return getJavaType().isDecimalType(); + } + + public boolean isNumericType() { + return getJavaType().isNumericType(); + } + + public boolean isDateType() { + return getJavaType().isDateType(); + } + + public boolean isTimeType() { + return getJavaType().isTimeType(); + } + + public boolean isDateTimeType() { + return getJavaType().isDateTimeType(); + } + + public boolean isAnyDateType() { + return getJavaType().isAnyDateType(); + } + + public boolean isBlobType() { + return this == TINYBLOB || this == MEDIUMBLOB || this == BLOB || this == LONGBLOB; + } + + public boolean isBinaryType() { + return getJavaType().isBinaryType(); + } + + public boolean hasPrecision(@NotNull DatabaseType dialect) { + return getDialectType(dialect).precision; + } + + public boolean hasScale(@NotNull DatabaseType dialect) { + return getDialectType(dialect).scale; + } + + public String getJdbcName(@NotNull DatabaseType dialect) { + DialectType dialectType = getDialectType(dialect); + return dialectType.getNameResolved(); + } + + public String formatSql(@NotNull DatabaseType dialect, Integer precision) { + return formatSql(dialect, precision, null); + } + + public String formatSql(@NotNull DatabaseType dialect) { + return formatSql(dialect, null, null); + } + + public String formatSql(@NotNull DatabaseType dialect, Integer precision, Integer scale) { + DialectType dialectType = getDialectType(dialect); + return dialectType.formatSql(precision, scale); + } + + public Object toJdbcValue(@NotNull DatabaseType dialect, Object value, boolean check) { + if (value != null && check && !javaType.getObjectType().matchType(value.getClass(), false)) { + throw new PlatformServiceUnavailableException("error.msg.database.type.not.allowed", + "Data type of parameter " + value + " does not match " + this); + } + return toJdbcValueImpl(dialect, value); + } + + public Object toJdbcValueImpl(@NotNull DatabaseType dialect, Object value) { + return value; + } + + @com.google.errorprone.annotations.Immutable + private static final class DialectType { + + @NotNull + private final JDBCType jdbcType; + @NotNull + private final String name; + private final boolean precision; + private final boolean scale; + private final ImmutableList alterNames; + + private DialectType(@NotNull JDBCType jdbcType, String name, boolean precision, boolean scale, String... alterNames) { + this.jdbcType = jdbcType; + this.name = name == null ? jdbcType.getName() : name; + this.precision = precision; + this.scale = scale; + this.alterNames = ImmutableList.copyOf(alterNames); + } + + private DialectType(@NotNull JDBCType jdbcType, String name, boolean precision, String... alterNames) { + this(jdbcType, name, precision, false, alterNames); + } + + private DialectType(@NotNull JDBCType jdbcType, String name, boolean precision) { + this(jdbcType, name, precision, false); + } + + private DialectType(@NotNull JDBCType jdbcType, String name, String... alterNames) { + this(jdbcType, name, false, false, alterNames); + } + + private DialectType(@NotNull JDBCType jdbcType, boolean precision, boolean scale) { + this(jdbcType, null, precision, scale); + } + + private DialectType(@NotNull JDBCType jdbcType, boolean precision) { + this(jdbcType, null, precision, false); + } + + private DialectType(@NotNull JDBCType jdbcType) { + this(jdbcType, null, false, false); + } + + private String getNameResolved() { + return name.replace(PLACEHOLDER_TYPE, jdbcType.getName()); + } + + private String formatSql(Integer precision, Integer scale) { + int idx = name.indexOf(PLACEHOLDER_TYPE); + if (idx >= 0) { + return name.substring(0, idx) + addSqlPrecisionScale(jdbcType.getName(), precision, scale) + + name.substring(idx + PLACEHOLDER_TYPE.length()); + } else { + return addSqlPrecisionScale(name, precision, scale); + } + } + + private String addSqlPrecisionScale(@NotNull String name, Integer precision, Integer scale) { + if (!this.precision || precision == null) { + return name; + } + String sql = name + '(' + precision; + if (this.scale || scale != null) { + sql = sql + ',' + scale; + } + sql += ')'; + return sql; + } + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/SqlOperator.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/SqlOperator.java new file mode 100644 index 000000000..9c4d65d20 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/SqlOperator.java @@ -0,0 +1,202 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.service.database; + +import static java.lang.String.format; + +import jakarta.validation.constraints.NotNull; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException; + +@AllArgsConstructor +@Getter +public enum SqlOperator { + + EQ("="), // + NEQ("<>"), // + GTE(">="), // + LTE("<="), // + GT(">"), // + LT("<"), // + LIKE("LIKE") { // + + @Override + public String formatImpl(@NotNull DatabaseSpecificSQLGenerator sqlGenerator, JdbcJavaType columnType, String definition, + String... values) { + return format("%s %s %s", definition, getSymbol(), sqlGenerator.formatValue(columnType, "%" + values[0] + "%")); + } + + @Override + public String formatPlaceholderImpl(String definition, String placeholder) { + return format("%s %s CONCAT('%%', %s, '%%')", definition, getSymbol(), placeholder); + } + }, + NLIKE("NOT LIKE") { // + + @Override + public String formatImpl(@NotNull DatabaseSpecificSQLGenerator sqlGenerator, JdbcJavaType columnType, String definition, + String... values) { + return format("%s %s %s", definition, getSymbol(), sqlGenerator.formatValue(columnType, "%" + values[0] + "%")); + } + + @Override + public String formatPlaceholderImpl(String definition, String placeholder) { + return format("%s %s CONCAT('%%', %s, '%%')", definition, getSymbol(), placeholder); + } + }, + BTW("BETWEEN", 2) { // + + @Override + public String formatImpl(@NotNull DatabaseSpecificSQLGenerator sqlGenerator, JdbcJavaType columnType, String definition, + String... values) { + return format("%s %s %s AND %s", definition, getSymbol(), sqlGenerator.formatValue(columnType, values[0]), + sqlGenerator.formatValue(columnType, values[1])); + } + + @Override + public String formatPlaceholderImpl(String definition, String placeholder) { + return format("%s %s %s AND %s", definition, getSymbol(), placeholder, placeholder); + } + }, + NBTW("NOT BETWEEN", 2) { // + + @Override + public String formatImpl(@NotNull DatabaseSpecificSQLGenerator sqlGenerator, JdbcJavaType columnType, String definition, + String... values) { + return format("%s %s %s AND %s", definition, getSymbol(), sqlGenerator.formatValue(columnType, values[0]), + sqlGenerator.formatValue(columnType, values[1])); + } + + @Override + public String formatPlaceholderImpl(String definition, String placeholder) { + return format("%s %s %s AND %s", definition, getSymbol(), placeholder, placeholder); + } + }, + IN("IN", -1) { // + + @Override + public String formatImpl(@NotNull DatabaseSpecificSQLGenerator sqlGenerator, JdbcJavaType columnType, String definition, + String... values) { + return format("%s %s (%s)", definition, getSymbol(), + Arrays.stream(values).map(e -> sqlGenerator.formatValue(columnType, e)).collect(Collectors.joining(", "))); + } + + @Override + public boolean isPlaceholderSupported() { + return false; + } + }, + NIN("NOT IN", -1) { // + + @Override + public String formatImpl(@NotNull DatabaseSpecificSQLGenerator sqlGenerator, JdbcJavaType columnType, String definition, + String... values) { + return format("%s %s (%s)", definition, getSymbol(), + Arrays.stream(values).map(e -> sqlGenerator.formatValue(columnType, e)).collect(Collectors.joining(", "))); + } + + @Override + public boolean isPlaceholderSupported() { + return false; + } + }, + NULL("IS NULL", 0), // + NNULL("IS NOT NULL", 0), // + ; + + private final String symbol; + private final int paramCount; + + SqlOperator(String symbol) { + this(symbol, 1); + } + + public boolean isDefault() { + return this == getDefault(); + } + + public static SqlOperator getDefault() { + return EQ; + } + + public boolean isListType() { + return paramCount < 0; + } + + public String formatSql(@NotNull DatabaseSpecificSQLGenerator sqlGenerator, JdbcJavaType columnType, String definition, String alias, + List values) { + return formatSql(sqlGenerator, columnType, definition, alias, values == null ? null : values.toArray(String[]::new)); + } + + public String formatSql(@NotNull DatabaseSpecificSQLGenerator sqlGenerator, JdbcJavaType columnType, String definition, String alias, + String... values) { + validateValues(values); + return formatImpl(sqlGenerator, columnType, sqlGenerator.alias(sqlGenerator.escape(definition), alias), values); + } + + protected String formatImpl(@NotNull DatabaseSpecificSQLGenerator sqlGenerator, JdbcJavaType columnType, String definition, + String... values) { + return paramCount == 0 ? format("%s %s", definition, symbol) + : format("%s %s %s", definition, symbol, sqlGenerator.formatValue(columnType, values[0])); + } + + public boolean isPlaceholderSupported() { + return true; + } + + public String formatPlaceholder(@NotNull DatabaseSpecificSQLGenerator sqlGenerator, String definition, String alias) { + return formatPlaceholder(sqlGenerator, definition, alias, "?"); + } + + public String formatPlaceholder(@NotNull DatabaseSpecificSQLGenerator sqlGenerator, String definition, String alias, + String placeholder) { + return formatPlaceholderImpl(sqlGenerator.alias(sqlGenerator.escape(definition), alias), placeholder); + } + + protected String formatPlaceholderImpl(String definition, String placeholder) { + if (!isPlaceholderSupported()) { + throw new UnsupportedOperationException("Placeholder is not supported for this operator"); + } + return paramCount == 0 ? format("%s %s", definition, symbol) : format("%s %s %s", definition, symbol, placeholder); + } + + public void validateValues(String... values) { + if (values == null ? paramCount != 0 : (paramCount < 0 ? values.length < -paramCount : values.length != paramCount)) { + throw new PlatformServiceUnavailableException("error.msg.database.operator.invalid", + "Number of parameters " + Arrays.toString(values) + " must be " + Math.abs(paramCount) + " on " + this); + } + } + + public void validateValues(List values) { + int size = values == null ? 0 : values.size(); + if (paramCount < 0 ? size < -paramCount : size != paramCount) { + throw new PlatformServiceUnavailableException("error.msg.database.operator.invalid", + "Number of parameters " + size + " must be " + Math.abs(paramCount) + " on " + this); + } + } + + @NotNull + public static SqlOperator forName(String name) { + return name == null ? getDefault() : SqlOperator.valueOf(name.toUpperCase()); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/data/ResultsetColumnHeaderData.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/data/ResultsetColumnHeaderData.java index e8592ab86..a1b930807 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/data/ResultsetColumnHeaderData.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/data/ResultsetColumnHeaderData.java @@ -18,10 +18,13 @@ */ package org.apache.fineract.infrastructure.dataqueries.data; +import jakarta.validation.constraints.NotNull; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.infrastructure.core.service.database.DatabaseType; +import org.apache.fineract.infrastructure.core.service.database.JdbcJavaType; /** * Immutable data object representing a resultset column. @@ -29,9 +32,9 @@ import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityEx public final class ResultsetColumnHeaderData implements Serializable { private final String columnName; - private String columnType; + private JdbcJavaType columnType; private final Long columnLength; - private final String columnDisplayType; + private final DisplayType columnDisplayType; private final boolean isColumnNullable; private final boolean isColumnPrimaryKey; private final boolean isColumnUnique; @@ -40,8 +43,7 @@ public final class ResultsetColumnHeaderData implements Serializable { private final List columnValues; private final String columnCode; - public static ResultsetColumnHeaderData basic(final String columnName, final String columnType) { - + public static ResultsetColumnHeaderData basic(final String columnName, final String columnType, DatabaseType dialect) { final Long columnLength = null; final boolean columnNullable = false; final boolean columnIsPrimaryKey = false; @@ -50,21 +52,20 @@ public final class ResultsetColumnHeaderData implements Serializable { final boolean columnIsUnique = false; final boolean columnIsIndexed = false; return new ResultsetColumnHeaderData(columnName, columnType, columnLength, columnNullable, columnIsPrimaryKey, columnValues, - columnCode, columnIsUnique, columnIsIndexed); + columnCode, columnIsUnique, columnIsIndexed, dialect); } public static ResultsetColumnHeaderData detailed(final String columnName, final String columnType, final Long columnLength, final boolean columnNullable, final boolean columnIsPrimaryKey, final List columnValues, - final String columnCode, final boolean columnIsUnique, final boolean columnIsIndexed) { + final String columnCode, final boolean columnIsUnique, final boolean columnIsIndexed, DatabaseType dialect) { return new ResultsetColumnHeaderData(columnName, columnType, columnLength, columnNullable, columnIsPrimaryKey, columnValues, - columnCode, columnIsUnique, columnIsIndexed); + columnCode, columnIsUnique, columnIsIndexed, dialect); } - private ResultsetColumnHeaderData(final String columnName, final String columnType, final Long columnLength, - final boolean columnNullable, final boolean columnIsPrimaryKey, final List columnValues, - final String columnCode, final boolean columnIsUnique, final boolean columnIsIndexed) { + private ResultsetColumnHeaderData(final String columnName, String columnType, final Long columnLength, final boolean columnNullable, + final boolean columnIsPrimaryKey, final List columnValues, final String columnCode, + final boolean columnIsUnique, final boolean columnIsIndexed, DatabaseType dialect) { this.columnName = columnName; - this.columnType = columnType; this.columnLength = columnLength; this.isColumnNullable = columnNullable; this.isColumnPrimaryKey = columnIsPrimaryKey; @@ -74,179 +75,20 @@ public final class ResultsetColumnHeaderData implements Serializable { this.isColumnIndexed = columnIsIndexed; // Refer org.drizzle.jdbc.internal.mysql.MySQLType.java - adjustColumnTypes(); + this.columnType = JdbcJavaType.getByTypeName(dialect, adjustColumnType(columnType)); - String displayType = null; - if (this.columnCode == null) { - if (isString()) { - displayType = "STRING"; - } else if (isAnyInteger()) { - if (isInteger()) { - this.columnType = this.columnType.toUpperCase(); - } - displayType = "INTEGER"; - } else if (isDate()) { - displayType = "DATE"; - } else if (isDateTime()) { - displayType = "DATETIME"; - } else if (isDecimal()) { - displayType = "DECIMAL"; - } else if (isAnyText()) { - displayType = "TEXT"; - } else if (isBit() || isBoolean()) { - displayType = "BOOLEAN"; - } else { - throw new PlatformDataIntegrityException("error.msg.invalid.lookup.type", - "Invalid Lookup Type:" + this.columnType + " - Column Name: " + this.columnName); - } - - } else { - if (isInt() || isInteger()) { - displayType = "CODELOOKUP"; - } else if (isVarchar()) { - displayType = "CODEVALUE"; - } else { - throw new PlatformDataIntegrityException("error.msg.invalid.lookup.type", - "Invalid Lookup Type:" + this.columnType + " - Column Name: " + this.columnName); - } - } - - this.columnDisplayType = displayType; - } - - private void adjustColumnTypes() { - switch (this.columnType) { - case "NEWDECIMAL": - this.columnType = "DECIMAL"; - break; - case "CLOB": - case "ENUM": - case "SET": - this.columnType = "varchar"; - break; - case "LONGLONG": - this.columnType = "bigint"; - break; - case "SHORT": - this.columnType = "smallint"; - break; - case "TINY": - this.columnType = "tinyint"; - break; - case "INT24": - this.columnType = "int"; - break; - default: - break; - } - } - - public boolean isNamed(final String columnName) { - return this.columnName.equalsIgnoreCase(columnName); - } - - private boolean isAnyText() { - return isText() || isTinyText() || isMediumText() || isLongText(); - } - - private boolean isText() { - return "text".equalsIgnoreCase(this.columnType); - } - - private boolean isTinyText() { - return "tinytext".equalsIgnoreCase(this.columnType); - } - - private boolean isMediumText() { - return "mediumtext".equalsIgnoreCase(this.columnType); - } - - private boolean isLongText() { - return "longtext".equalsIgnoreCase(this.columnType); - } - - private boolean isDecimal() { - return "decimal".equalsIgnoreCase(this.columnType) || "NEWDECIMAL".equalsIgnoreCase(this.columnType) - || "numeric".equalsIgnoreCase(this.columnType); - // Refer org.drizzle.jdbc.internal.mysql.MySQLType.java - } - - private boolean isDate() { - return "date".equalsIgnoreCase(this.columnType); - } - - private boolean isDateTime() { - return "datetime".equalsIgnoreCase(this.columnType) || "timestamp".equalsIgnoreCase(this.columnType) - || "timestamp without time zone".equalsIgnoreCase(this.columnType) || "timestamptz".equalsIgnoreCase(this.columnType) - || "timestamp with time zone".equalsIgnoreCase(this.columnType); - } - - public boolean isString() { - return isVarchar() || isChar(); - } - - private boolean isChar() { - return "char".equalsIgnoreCase(this.columnType) || "CHARACTER VARYING".equalsIgnoreCase(this.columnType) - || "bpchar".equalsIgnoreCase(this.columnType); - } - - private boolean isVarchar() { - return "varchar".equalsIgnoreCase(this.columnType); - } - - private boolean isAnyInteger() { - return isInt() || isInteger() || isSmallInt() || isTinyInt() || isMediumInt() || isBigInt() || isLong() || isSerial(); - } - - private boolean isSerial() { - return "SERIAL".equalsIgnoreCase(this.columnType) || "SERIAL4".equalsIgnoreCase(this.columnType) - || "SERIAL8".equalsIgnoreCase(this.columnType) || "SMALLSERIAL".equalsIgnoreCase(this.columnType) - || "BIGSERIAL".equalsIgnoreCase(this.columnType); - } - - private boolean isInt() { - return "int".equalsIgnoreCase(this.columnType); - } - - private boolean isInteger() { - return "integer".equalsIgnoreCase(this.columnType) || "int2".equalsIgnoreCase(this.columnType) - || "int4".equalsIgnoreCase(this.columnType); - } - - private boolean isSmallInt() { - return "smallint".equalsIgnoreCase(this.columnType); - } - - private boolean isTinyInt() { - return "tinyint".equalsIgnoreCase(this.columnType); - } - - private boolean isMediumInt() { - return "mediumint".equalsIgnoreCase(this.columnType); - } - - private boolean isBigInt() { - return "bigint".equalsIgnoreCase(this.columnType) || "int8".equalsIgnoreCase(this.columnType); - } - - private boolean isLong() { - return "LONG".equalsIgnoreCase(this.columnType) || "LONGLONG".equalsIgnoreCase(this.columnType); - // Refer org.drizzle.jdbc.internal.mysql.MySQLType.java - } - - private boolean isBit() { - return "bit".equalsIgnoreCase(this.columnType); - } - - private boolean isBoolean() { - return "boolean".equalsIgnoreCase(this.columnType) || "bool".equalsIgnoreCase(this.columnType); + this.columnDisplayType = calcDisplayType(); } public String getColumnName() { return this.columnName; } - public String getColumnType() { + public boolean isNamed(final String columnName) { + return this.columnName.equalsIgnoreCase(columnName); + } + + public JdbcJavaType getColumnType() { return this.columnType; } @@ -270,78 +112,10 @@ public final class ResultsetColumnHeaderData implements Serializable { return isColumnIndexed; } - public String getColumnDisplayType() { + public DisplayType getColumnDisplayType() { return this.columnDisplayType; } - public boolean isDateDisplayType() { - return "DATE".equalsIgnoreCase(this.columnDisplayType); - } - - public boolean isDateTimeDisplayType() { - return "DATETIME".equalsIgnoreCase(this.columnDisplayType); - } - - public boolean isIntegerDisplayType() { - return "INTEGER".equalsIgnoreCase(this.columnDisplayType); - } - - public boolean isDecimalDisplayType() { - return "DECIMAL".equalsIgnoreCase(this.columnDisplayType); - } - - public boolean isBooleanDisplayType() { - return "BOOLEAN".equalsIgnoreCase(this.columnDisplayType); - } - - public boolean isCodeValueDisplayType() { - return "CODEVALUE".equalsIgnoreCase(this.columnDisplayType); - } - - public boolean isCodeLookupDisplayType() { - return "CODELOOKUP".equalsIgnoreCase(this.columnDisplayType); - } - - public boolean isMandatory() { - return !isOptional(); - } - - public boolean isOptional() { - return this.isColumnNullable; - } - - public boolean hasColumnValues() { - return !this.columnValues.isEmpty(); - } - - public boolean isColumnValueAllowed(final String match) { - boolean allowed = false; - for (final ResultsetColumnValueData allowedValue : this.columnValues) { - if (allowedValue.matches(match)) { - allowed = true; - } - } - return allowed; - } - - public boolean isColumnValueNotAllowed(final String match) { - return !isColumnValueAllowed(match); - } - - public boolean isColumnCodeNotAllowed(final Integer match) { - return !isColumnCodeAllowed(match); - } - - public boolean isColumnCodeAllowed(final Integer match) { - boolean allowed = false; - for (final ResultsetColumnValueData allowedValue : this.columnValues) { - if (allowedValue.codeMatches(match)) { - allowed = true; - } - } - return allowed; - } - public String getColumnCode() { return this.columnCode; } @@ -349,4 +123,156 @@ public final class ResultsetColumnHeaderData implements Serializable { public List getColumnValues() { return this.columnValues; } + + public boolean isDateDisplayType() { + return columnDisplayType == DisplayType.DATE; + } + + public boolean isDateTimeDisplayType() { + return columnDisplayType == DisplayType.DATETIME; + } + + public boolean isTimeDisplayType() { + return columnDisplayType == DisplayType.TIME; + } + + public boolean isIntegerDisplayType() { + return columnDisplayType == DisplayType.INTEGER; + } + + public boolean isDecimalDisplayType() { + return columnDisplayType == DisplayType.DECIMAL; + } + + public boolean isStringDisplayType() { + return columnDisplayType == DisplayType.STRING; + } + + public boolean isTextDisplayType() { + return columnDisplayType == DisplayType.TEXT; + } + + public boolean isBooleanDisplayType() { + return columnDisplayType == DisplayType.BOOLEAN; + } + + public boolean isCodeValueDisplayType() { + return columnDisplayType == DisplayType.CODEVALUE; + } + + public boolean isCodeLookupDisplayType() { + return columnDisplayType == DisplayType.CODELOOKUP; + } + + public boolean isMandatory() { + return !this.isColumnNullable; + } + + public boolean hasColumnValues() { + return columnValues != null && !columnValues.isEmpty(); + } + + public boolean isColumnValueAllowed(final String match) { + for (final ResultsetColumnValueData allowedValue : this.columnValues) { + if (allowedValue.matches(match)) { + return true; + } + } + return false; + } + + public boolean isColumnCodeAllowed(final Integer match) { + for (final ResultsetColumnValueData allowedValue : this.columnValues) { + if (allowedValue.codeMatches(match)) { + return true; + } + } + return false; + } + + public boolean hasPrecision(@NotNull DatabaseType dialect) { + return columnType.hasPrecision(dialect); + } + + // --- Calculation --- + + private String adjustColumnType(String type) { + type = type.toUpperCase(); + switch (type) { + case "CLOB": + case "ENUM": + case "SET": + return "VARCHAR"; + case "NEWDECIMAL": + return "DECIMAL"; + case "LONGLONG": + return "BIGINT"; + case "SHORT": + return "SMALLINT"; + case "TINY": + return "TINYINT"; + case "INT24": + return "INT"; + default: + return type; + } + } + + @NotNull + private DisplayType calcDisplayType() { + DisplayType displayType = null; + if (this.columnCode == null) { + displayType = calcColumnDisplayType(columnType); + } else { + if (columnType.isIntegerType()) { + displayType = DisplayType.CODELOOKUP; + } else if (columnType.isVarcharType()) { + displayType = DisplayType.CODEVALUE; + } + + } + if (displayType == null) { + throw new PlatformDataIntegrityException("error.msg.invalid.lookup.type", + "Invalid Lookup Type:" + this.columnType + " - Column Name: " + this.columnName); + } + return displayType; + } + + public static DisplayType calcColumnDisplayType(JdbcJavaType columnType) { + if (columnType.isTextType()) { + return DisplayType.TEXT; + } + if (columnType.isStringType()) { + return DisplayType.STRING; + } + if (columnType.isAnyIntegerType()) { + return DisplayType.INTEGER; + } + if (columnType.isAnyFloatType()) { + return DisplayType.FLOAT; + } + if (columnType.isDecimalType()) { // Refer org.drizzle.jdbc.internal.mysql.MySQLType.java + return DisplayType.DECIMAL; + } + if (columnType.isDateType()) { + return DisplayType.DATE; + } + if (columnType.isDateTimeType()) { + return DisplayType.DATETIME; + } + if (columnType.isTimeType()) { + return DisplayType.TIME; + } + if (columnType.isBooleanType()) { + return DisplayType.BOOLEAN; + } + if (columnType.isBinaryType()) { + return DisplayType.BINARY; + } + return null; + } + + public enum DisplayType { + TEXT, STRING, INTEGER, FLOAT, DECIMAL, DATE, TIME, DATETIME, BOOLEAN, BINARY, CODELOOKUP, CODEVALUE,; + } } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java b/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java index c85f39342..65a6b4edf 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java @@ -26,9 +26,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.cob.loan.LoanCOBBusinessStep; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAccountSnapshotBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; -import org.apache.fineract.interoperation.util.MathUtil; import org.apache.fineract.investor.config.InvestorModuleIsEnabledCondition; import org.apache.fineract.investor.data.ExternalTransferStatus; import org.apache.fineract.investor.data.ExternalTransferSubStatus; diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceImpl.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceImpl.java index 69aa5da5a..df211085f 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceImpl.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceImpl.java @@ -34,9 +34,9 @@ import java.util.List; import java.util.Objects; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAccountSnapshotBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; -import org.apache.fineract.interoperation.util.MathUtil; import org.apache.fineract.investor.data.ExternalTransferStatus; import org.apache.fineract.investor.data.ExternalTransferSubStatus; import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index facf6bb29..84369c0d7 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -71,8 +71,8 @@ import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidati import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper; 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.security.service.RandomPasswordGenerator; -import org.apache.fineract.interoperation.util.MathUtil; import org.apache.fineract.organisation.holiday.domain.Holiday; import org.apache.fineract.organisation.holiday.service.HolidayUtil; import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java index 5637d5f59..1b962a903 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java @@ -25,7 +25,7 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import java.math.BigDecimal; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; -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.LoanInstallmentChargeData; diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java index 2bea7c024..1989ba82d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java @@ -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 SUPPORTED_PARAMETERS_FOR_CREATE = new HashSet<>( - Arrays.asList(DATATABLE_NAME, ENTITY_SUB_TYPE, APPTABLE_NAME, MULTI_ROW, COLUMNS)); - private static final Set SUPPORTED_PARAMETERS_FOR_CREATE_COLUMNS = new HashSet<>( - Arrays.asList(NAME, TYPE, LENGTH, MANDATORY, CODE, UNIQUE, INDEXED)); - private static final Set SUPPORTED_PARAMETERS_FOR_UPDATE = new HashSet<>( - Arrays.asList(APPTABLE_NAME, ENTITY_SUB_TYPE, CHANGE_COLUMNS, ADD_COLUMNS, DROP_COLUMNS)); - private static final Set SUPPORTED_PARAMETERS_FOR_ADD_COLUMNS = new HashSet<>( - Arrays.asList(NAME, TYPE, LENGTH, MANDATORY, AFTER, CODE, UNIQUE, INDEXED)); - private static final Set SUPPORTED_PARAMETERS_FOR_CHANGE_COLUMNS = new HashSet<>( - Arrays.asList(NAME, NEW_NAME, LENGTH, MANDATORY, AFTER, CODE, NEW_CODE, UNIQUE, INDEXED)); - private static final Set 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 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 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 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 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 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 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>() { - - }.getType(); + final Type typeOfMap = new TypeToken>() {}.getType(); this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS_FOR_CREATE); final List 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 dataValidationErrors) { if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DataTableApiConstant.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DataTableApiConstant.java index 76ca161e1..0028c2070 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DataTableApiConstant.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DataTableApiConstant.java @@ -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"; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/data/EntityTables.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/data/EntityTables.java index bd0b5a399..fde2316ab 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/data/EntityTables.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/data/EntityTables.java @@ -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 lookup = new HashMap(); + public static final EntityTables[] VALUES = values(); - static { - for (EntityTables d : EntityTables.values()) { - lookup.put(d.getName(), d); - } - } + private static final List ENTITY_NAMES = Arrays.stream(VALUES).map(EntityTables::getName).toList(); + + private static final Map BY_ENTITY_NAME = Arrays.stream(VALUES) + .collect(Collectors.toMap(EntityTables::getName, e -> e)); private final String name; - - private final ImmutableList codes; + private final String apptableName; private final String foreignKeyColumnNameOnDatatable; + private final String refColumn; // referenced column name on apptable - EntityTables(String name, ImmutableList codes, String foreignKeyColumnNameOnDatatable) { + private final ImmutableList 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 getEntitiesList() { - - List data = new ArrayList(); - - for (EntityTables entity : EntityTables.values()) { - data.add(entity.name); - } - - return data; - - } - - public static ImmutableList 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 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 getCheckStatuses() { + return checkStatuses; + } + + public boolean hasCheck() { + return checkStatuses != null && !checkStatuses.isEmpty(); + } + + public static List 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 getCheckStatuses(String name) { + EntityTables entityTable = fromEntityName(name); + return entityTable == null ? List.of() : entityTable.getCheckStatuses(); + } + + @NotNull + public static List getCheckStatusCodes(String name) { + return getCheckStatuses(name).stream().map(StatusEnum::getCode).toList(); + } + + @NotNull + public static List getFiltered(Predicate filter) { + return Arrays.stream(VALUES).filter(filter).toList(); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/data/GenericResultsetData.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/data/GenericResultsetData.java index a2bf949e8..21e52f7f1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/data/GenericResultsetData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/data/GenericResultsetData.java @@ -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() { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/exception/DatatableNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/exception/DatatableNotFoundException.java index 072527a81..00d57bcd9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/exception/DatatableNotFoundException.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/exception/DatatableNotFoundException.java @@ -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()); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksDataValidator.java index 7802fde24..abed6d817 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksDataValidator.java @@ -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 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); diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksReadPlatformServiceImpl.java index 67eae4685..3e5afd9de 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksReadPlatformServiceImpl.java @@ -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 dataTables = getDataTables(); - List entities = EntityTables.getEntitiesList(); - List statusClient = getStatusList(EntityTables.getStatus("m_client")); - List statusLoan = getStatusList(EntityTables.getStatus("m_loan")); - List statusGroup = getStatusList(EntityTables.getStatus("m_group")); - List statusSavings = getStatusList(EntityTables.getStatus("m_savings_account")); + List entities = EntityTables.getEntityNames(); + List clientStatuses = getStatusList(EntityTables.CLIENT.getCheckStatuses()); + List loanStatuses = getStatusList(EntityTables.LOAN.getCheckStatuses()); + List groupstatuses = getStatusList(EntityTables.GROUP.getCheckStatuses()); + List savingsStatuses = getStatusList(EntityTables.SAVINGS.getCheckStatuses()); Collection loanProductDatas = this.loanProductReadPlatformService.retrieveAllLoanProductsForLookup(true); Collection 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 getStatusList(ImmutableList statuses) { + private List getStatusList(List statuses) { List 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; diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksWritePlatformServiceImpl.java index 9279a12c5..dc42431a4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksWritePlatformServiceImpl.java @@ -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 { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/GenericDataService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/GenericDataService.java index 0d239578c..44a24f1f6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/GenericDataService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/GenericDataService.java @@ -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 fillResultsetColumnHeaders(String tableName); + + List fillResultsetRowData(String sql, List columnHeaders); + String generateJsonFromGenericResultsetData(GenericResultsetData grs); String replace(String str, String pattern, String replace); String wrapSQL(String sql); - List 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); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/GenericDataServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/GenericDataServiceImpl.java index 4c166767a..924d62d3b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/GenericDataServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/GenericDataServiceImpl.java @@ -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 columnHeaders = new ArrayList<>(); - final List 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 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 resultsetDataRows = fillResultsetRowData(rs, columnHeaders); return new GenericResultsetData(columnHeaders, resultsetDataRows); } catch (DataAccessException e) { @@ -98,6 +91,80 @@ public class GenericDataServiceImpl implements GenericDataService { } } + @Override + public List fillResultsetColumnHeaders(final String tableName) { + final SqlRowSet columnDefinitions = getTableMetaData(tableName); + final List indexDefinitions = getDatatableIndexData(tableName); + + DatabaseType dialect = databaseTypeResolver.databaseType(); + final List 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 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 fillResultsetRowData(final String sql, List columnHeaders) { + final SqlRowSet rs = jdbcTemplate.queryForRowSet(sql); // NOSONAR + return fillResultsetRowData(rs, columnHeaders); + } + + @NotNull + private static List fillResultsetRowData(SqlRowSet rs, List columnHeaders) { + final SqlRowSetMetaData rsmd = rs.getMetaData(); + final List resultsetDataRows = new ArrayList<>(); + while (rs.next()) { + final List 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 fillResultsetColumnHeaders(final String datatable) { - final SqlRowSet columnDefinitions = getDatatableMetaData(datatable); - final List indexDefinitions = getDatatableIndexData(datatable); - - final List 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 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 indexDefinitions) { - String keyNameToCheck = datatableKeywordGenerator.generateUniqueKeyName(datatable, columnName); + private boolean isExplicitlyUnique(String tableName, String columnName, List 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 indexDefinitions) { - String keyNameToCheck = datatableKeywordGenerator.generateIndexName(datatable, columnName); + private boolean isExplicitlyIndexed(String tableName, String columnName, List 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 indexDefinitions) { @@ -276,25 +291,23 @@ public class GenericDataServiceImpl implements GenericDataService { } @SuppressWarnings("AvoidHidingCauseException") - private List getDatatableIndexData(String datatable) { + private List 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 retrieveColumnValues(final String codeName) { + private List 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 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); } } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadReportingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadReportingServiceImpl.java index 232d86010..16fa4b5f0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadReportingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadReportingServiceImpl.java @@ -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()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java index 25456173c..d62d3090f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java @@ -19,25 +19,43 @@ package org.apache.fineract.infrastructure.dataqueries.service; import static java.util.Arrays.asList; -import static java.util.Locale.ENGLISH; -import static org.apache.fineract.infrastructure.core.data.ApiParameterError.parameterErrorWithValue; +import static org.apache.fineract.infrastructure.core.serialization.DatatableCommandFromApiJsonDeserializer.DATATABLE_NAME_REGEX_PATTERN; +import static org.apache.fineract.infrastructure.core.service.database.JdbcJavaType.BIT; +import static org.apache.fineract.infrastructure.core.service.database.JdbcJavaType.DATETIME; +import static org.apache.fineract.infrastructure.core.service.database.SqlOperator.EQ; +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_DROPDOWN; +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 static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.TABLE_REGISTERED_TABLE; import com.google.common.base.Splitter; -import com.google.common.collect.ImmutableMap; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; import jakarta.persistence.PersistenceException; +import jakarta.validation.constraints.NotNull; import java.lang.reflect.Type; import java.math.BigDecimal; -import java.sql.Date; import java.sql.SQLException; import java.sql.Statement; -import java.sql.Timestamp; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -64,9 +82,11 @@ import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityEx import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException; import org.apache.fineract.infrastructure.core.serialization.DatatableCommandFromApiJsonDeserializer; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; -import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; +import org.apache.fineract.infrastructure.core.service.database.DatabaseType; import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver; +import org.apache.fineract.infrastructure.core.service.database.JdbcJavaType; import org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant; import org.apache.fineract.infrastructure.dataqueries.data.DataTableValidator; import org.apache.fineract.infrastructure.dataqueries.data.DatatableData; @@ -81,6 +101,7 @@ import org.apache.fineract.infrastructure.security.service.PlatformSecurityConte import org.apache.fineract.infrastructure.security.service.SqlInjectionPreventerService; import org.apache.fineract.infrastructure.security.utils.ColumnValidator; import org.apache.fineract.infrastructure.security.utils.SQLInjectionValidator; +import org.apache.fineract.portfolio.search.service.SearchUtil; import org.apache.fineract.useradministration.domain.AppUser; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataIntegrityViolationException; @@ -90,7 +111,6 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.KeyHolder; import org.springframework.jdbc.support.rowset.SqlRowSet; -import org.springframework.jdbc.support.rowset.SqlRowSetMetaData; import org.springframework.orm.jpa.JpaSystemException; import org.springframework.transaction.annotation.Transactional; @@ -98,34 +118,13 @@ import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataService { - 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 CODE_VALUES_TABLE = "m_code_value"; - // TODO: Extract these types out of here - private static final ImmutableMap apiTypeToMySQL = ImmutableMap.builder().put("string", "VARCHAR") - .put("number", "INT").put("boolean", "BIT").put("decimal", "DECIMAL").put("date", "DATE").put("datetime", "DATETIME") - .put("text", "TEXT").put("dropdown", "INT").build(); - private static final ImmutableMap apiTypeToPostgreSQL = ImmutableMap.builder().put("string", "VARCHAR") - .put("number", "INT").put("boolean", "boolean").put("decimal", "DECIMAL").put("date", "DATE").put("datetime", "TIMESTAMP") - .put("text", "TEXT").put("dropdown", "INT").build(); - - private static final List stringDataTypes = asList("char", "varchar", "blob", "text", "tinyblob", "tinytext", "mediumblob", - "mediumtext", "longblob", "longtext"); - private static final String DATE_FORMAT = "yyyy-MM-dd"; - public static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; - private static final DateTimeFormatter DATA_TABLE_DATE_FORMAT = DateTimeFormatter.ofPattern(DATE_FORMAT); - private static final DateTimeFormatter DATA_TABLE_DATETIME_FORMAT = DateTimeFormatter.ofPattern(DATE_TIME_FORMAT); - - public static final String MYSQL_DATETIME_TYPE = "datetime"; - public static final String POSTGRES_TIMESTAMP_TYPE = "timestamp without time zone"; - private final JdbcTemplate jdbcTemplate; private final DatabaseTypeResolver databaseTypeResolver; private final DatabaseSpecificSQLGenerator sqlGenerator; private final PlatformSecurityContext context; private final FromJsonHelper fromJsonHelper; - private final JsonParserHelper helper = new JsonParserHelper(); private final GenericDataService genericDataService; private final DatatableCommandFromApiJsonDeserializer fromApiJsonDeserializer; private final ConfigurationDomainService configurationDomainService; @@ -138,16 +137,19 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ @Override public List retrieveDatatableNames(final String appTable) { - Object[] params = new Object[] { this.context.authenticatedUser().getId() }; // PERMITTED datatables - String sql = "select application_table_name, registered_table_name, entity_subtype " + " from x_registered_table " + " where exists" - + " (select 'f'" + " from m_appuser_role ur " + " join m_role r on r.id = ur.role_id" - + " left join m_role_permission rp on rp.role_id = r.id" + " left join m_permission p on p.id = rp.permission_id" + String sql = "select application_table_name, registered_table_name, entity_subtype from x_registered_table where exists" + + " (select 'f' from m_appuser_role ur join m_role r on r.id = ur.role_id" + + " left join m_role_permission rp on rp.role_id = r.id left join m_permission p on p.id = rp.permission_id" + " where ur.appuser_id = ? and (p.code in ('ALL_FUNCTIONS', 'ALL_FUNCTIONS_READ') or p.code = concat" + "('READ_', registered_table_name))) "; + + Object[] params; if (appTable != null) { sql = sql + " and application_table_name like ? "; params = new Object[] { this.context.authenticatedUser().getId(), appTable }; + } else { + params = new Object[] { this.context.authenticatedUser().getId() }; } sql = sql + " order by application_table_name, registered_table_name"; @@ -168,7 +170,6 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ @Override public DatatableData retrieveDatatable(final String datatable) { - // PERMITTED datatables SQLInjectionValidator.validateSQLInput(datatable); final String sql = "select application_table_name, registered_table_name, entity_subtype from x_registered_table " @@ -195,133 +196,49 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } @Override - public List queryDataTable(String datatable, final String columnFilter, String valueFilter, String resultColumns) { - asList(datatable, columnFilter, valueFilter, resultColumns).forEach(SQLInjectionValidator::validateDynamicQuery); - validateDatatable(datatable); + public List queryDataTable(@NotNull String datatable, @NotNull String columnName, String columnValueString, + @NotNull String resultColumnsString) { + datatable = validateDatatableRegistered(datatable); + Map columnHeaders = SearchUtil + .mapHeadersToName(genericDataService.fillResultsetColumnHeaders(datatable)); - List resultsetColumnHeaderData = genericDataService.fillResultsetColumnHeaders(datatable); - validateRequestParams(columnFilter, valueFilter, resultColumns, resultsetColumnHeaderData); + List errors = new ArrayList<>(); + columnName = SearchUtil.validateToJdbcColumn(columnName, columnHeaders, errors, false); + List resultColumns = asList(resultColumnsString.split(",")); + List selectColumns = SearchUtil.validateToJdbcColumns(resultColumns, columnHeaders, errors, false); + if (!errors.isEmpty()) { + throw new PlatformApiDataValidationException(errors); + } - Object validatedValueFilter = getValidatedValueFilter(columnFilter, valueFilter, resultsetColumnHeaderData); - String sql = "select " + escapeFieldNames(resultColumns) + " from " + sqlGenerator.escape(datatable) + " where " - + escapeFieldNames(columnFilter) + " = ?"; - SqlRowSet rowSet = jdbcTemplate.queryForRowSet(sql, validatedValueFilter); + DatabaseType dialect = databaseTypeResolver.databaseType(); + Object columnValue = SearchUtil.parseAndValidateJdbcColumnValue(columnName, columnValueString, columnHeaders, dialect); + String sql = sqlGenerator.buildSelect(selectColumns, null, false) + " " + sqlGenerator.buildFrom(datatable, null, false) + " WHERE " + + EQ.formatPlaceholder(sqlGenerator, columnName, null); + SqlRowSet rowSet = jdbcTemplate.queryForRowSet(sql, columnValue); - String[] resultColumnNames = resultColumns.split(","); List results = new ArrayList<>(); while (rowSet.next()) { - extractResults(rowSet, resultColumnNames, results); + SearchUtil.extractJsonResult(rowSet, selectColumns, resultColumns, results); } - return results; } - private Object getValidatedValueFilter(String columnFilter, String valueFilter, - List resultsetColumnHeaderData) { - ResultsetColumnHeaderData columnFilterHeader = resultsetColumnHeaderData.stream() - .filter(headerData -> headerData.getColumnName().equals(columnFilter)).findAny().orElseThrow(); - if (MYSQL_DATETIME_TYPE.equals(columnFilterHeader.getColumnType()) - || POSTGRES_TIMESTAMP_TYPE.equals(columnFilterHeader.getColumnType())) { - return validateColumn(columnFilterHeader, valueFilter, DATE_TIME_FORMAT, ENGLISH); - } else { - return validateColumn(columnFilterHeader, valueFilter, DATE_FORMAT, ENGLISH); - } - } - - private void extractResults(SqlRowSet rowSet, String[] resultColumnNames, List results) { - JsonObject json = new JsonObject(); - for (String rcn : resultColumnNames) { - rcn = rcn.replaceAll("\"", ""); - Object rowValue = rowSet.getObject(rcn); - if (rowValue != null) { - if (rowValue instanceof Character) { - json.addProperty(rcn, (Character) rowValue); - } else if (rowValue instanceof Number) { - json.addProperty(rcn, new BigDecimal(rowValue.toString())); - } else if (rowValue instanceof Boolean) { - json.addProperty(rcn, (Boolean) rowValue); - } else if (rowValue instanceof LocalDateTime) { - json.addProperty(rcn, DATA_TABLE_DATETIME_FORMAT.format((LocalDateTime) rowValue)); - } else if (rowValue instanceof Timestamp) { - json.addProperty(rcn, DATA_TABLE_DATETIME_FORMAT.format(((Timestamp) rowValue).toLocalDateTime())); - } else if (rowValue instanceof LocalDate) { - json.addProperty(rcn, DATA_TABLE_DATE_FORMAT.format((LocalDate) rowValue)); - } else if (rowValue instanceof Date) { - json.addProperty(rcn, DATA_TABLE_DATE_FORMAT.format(((Date) rowValue).toLocalDate())); - } else { - json.addProperty(rcn, rowValue.toString()); - } - } - } - - if (json.size() > 0) { - results.add(json); - } - } - - private String escapeFieldNames(final String inputFields) { - final String delimiter = ","; - if (StringUtils.isBlank(inputFields)) { - return inputFields; - } - final String[] fieldList = StringUtils.split(inputFields, delimiter); - final String[] outputFields = new String[fieldList.length]; - for (int i = 0; i < fieldList.length; i++) { - outputFields[i] = sqlGenerator.escape(fieldList[i].trim()); - } - return String.join(",", outputFields); - } - - private void validateRequestParams(String columnFilter, String valueFilter, String resultColumns, - List resultsetColumnHeaderData) { - List paramErrors = new ArrayList<>(); - List dataTableColumnNames = resultsetColumnHeaderData.stream().map(ResultsetColumnHeaderData::getColumnName).toList(); - if (columnFilter == null || columnFilter.isEmpty()) { - paramErrors.add(parameterErrorWithValue("400", "Column filter is empty!", "columnFilter", columnFilter)); - } else { - if (!dataTableColumnNames.contains(columnFilter)) { - paramErrors.add(parameterErrorWithValue("400", "Column filter not exist in datatable!", "columnFilter", columnFilter)); - } - } - - if (valueFilter == null || valueFilter.isEmpty()) { - paramErrors.add(parameterErrorWithValue("400", "Value filter is empty!", "valueFilter", valueFilter)); - } - - if (resultColumns == null || resultColumns.isEmpty()) { - paramErrors.add(parameterErrorWithValue("400", "Result columns filter is empty!", "resultColumns", resultColumns)); - } else { - asList(resultColumns.split(",")).forEach(rcn -> { - if (!dataTableColumnNames.contains(rcn)) { - paramErrors.add(parameterErrorWithValue("400", "Result column not exist in datatable!", "resultColumns", rcn)); - } - }); - } - - if (!paramErrors.isEmpty()) { - throw new PlatformApiDataValidationException(paramErrors); - } - } - private void logAsErrorUnexpectedDataIntegrityException(final Exception dve) { log.error("Error occurred.", dve); } @Transactional @Override - public void registerDatatable(final String dataTableName, final String applicationTableName, final String entitySubType) { - + public void registerDatatable(final String dataTableName, final String entityName, final String entitySubType) { Integer category = DataTableApiConstant.CATEGORY_DEFAULT; final String permissionSql = this.getPermissionSql(dataTableName); - this.registerDataTable(applicationTableName, dataTableName, entitySubType, category, permissionSql); - + this.registerDataTable(entityName, dataTableName, entitySubType, category, permissionSql); } @Transactional @Override public void registerDatatable(final JsonCommand command) { - final String applicationTableName = this.getTableName(command.getUrl()); final String dataTableName = this.getDataTableName(command.getUrl()); final String entitySubType = command.stringValueOfParameterNamed("entitySubType"); @@ -331,7 +248,6 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ this.dataTableValidator.validateDataTableRegistration(command.json()); final String permissionSql = this.getPermissionSql(dataTableName); this.registerDataTable(applicationTableName, dataTableName, entitySubType, category, permissionSql); - } @Transactional @@ -346,20 +262,20 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ this.dataTableValidator.validateDataTableRegistration(command.json()); this.registerDataTable(applicationTableName, dataTableName, entitySubType, category, permissionSql); - } - private void registerDataTable(final String applicationTableName, final String dataTableName, final String entitySubType, - final Integer category, final String permissionsSql) { - - validateAppTable(applicationTableName); + private void registerDataTable(final String entityName, final String dataTableName, final String entitySubType, final Integer category, + final String permissionsSql) { + EntityTables entityTable = resolveEntity(entityName); validateDatatableName(dataTableName); - assertDataTableExists(dataTableName); + validateDataTableExists(dataTableName); Map paramMap = new HashMap<>(3); - final String registerDatatableSql = "insert into x_registered_table (registered_table_name, application_table_name, entity_subtype, category) values ( :dataTableName, :applicationTableName, :entitySubType ,:category)"; + final String registerDatatableSql = "insert into x_registered_table " + + "(registered_table_name, application_table_name, entity_subtype, category) " + + "values (:dataTableName, :applicationTableName, :entitySubType, :category)"; paramMap.put("dataTableName", dataTableName); - paramMap.put("applicationTableName", applicationTableName); + paramMap.put("applicationTableName", entityName); paramMap.put("entitySubType", entitySubType); paramMap.put("category", category); @@ -380,7 +296,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ // show duplicate datatable error msg if (realCause.getMessage().contains("Duplicate entry") || cause.getMessage().contains("Duplicate entry")) { throw new PlatformDataIntegrityException("error.msg.datatable.registered", - "Datatable `" + dataTableName + "` is already registered against an application table.", "dataTableName", + "Datatable `" + dataTableName + "` is already registered against an application table.", API_PARAM_DATATABLE_NAME, dataTableName, dve); } logAsErrorUnexpectedDataIntegrityException(dve); @@ -390,14 +306,13 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ final Throwable cause = dve.getCause(); if (cause.getMessage().contains("Duplicate entry")) { throw new PlatformDataIntegrityException("error.msg.datatable.registered", - "Datatable `" + dataTableName + "` is already registered against an application table.", "dataTableName", + "Datatable `" + dataTableName + "` is already registered against an application table.", API_PARAM_DATATABLE_NAME, dataTableName, dve); } logAsErrorUnexpectedDataIntegrityException(dve); throw new PlatformDataIntegrityException("error.msg.unknown.data.integrity.issue", "Unknown data integrity issue with resource.", dve); } - } private String getPermissionSql(final String dataTableName) { @@ -418,7 +333,6 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ + ", 'UPDATE', '" + dataTableName + "', true)," + "('datatable', " + updatePermissionChecker + ", 'UPDATE', '" + dataTableName + "', false)," + "('datatable', " + deletePermission + ", 'DELETE', '" + dataTableName + "', true)," + "('datatable', " + deletePermissionChecker + ", 'DELETE', '" + dataTableName + "', false)"; - } private Integer getCategory(final JsonCommand command) { @@ -433,27 +347,24 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ return category.equals(DataTableApiConstant.CATEGORY_PPI); } - private JsonElement addColumn(final String name, final String dataType, final boolean isMandatory, final Integer length, + private JsonElement addColumn(final String name, final JdbcJavaType dataType, final boolean isMandatory, final Integer length, final boolean isUnique, final boolean isIndexed) { JsonObject column = new JsonObject(); - column.addProperty("name", name); - column.addProperty("type", dataType); - if (dataType.equalsIgnoreCase("string")) { - column.addProperty("length", length); + column.addProperty(API_FIELD_NAME, name); + column.addProperty(API_FIELD_TYPE, dataType.formatSql(databaseTypeResolver.databaseType())); + if (dataType.isStringType()) { + column.addProperty(API_FIELD_LENGTH, length); } - column.addProperty("mandatory", (isMandatory ? "true" : "false")); - column.addProperty("unique", (isUnique ? "true" : "false")); - column.addProperty("indexed", (isIndexed ? "true" : "false")); + column.addProperty(API_FIELD_MANDATORY, (isMandatory ? "true" : "false")); + column.addProperty(API_FIELD_UNIQUE, (isUnique ? "true" : "false")); + column.addProperty(API_FIELD_INDEXED, (isIndexed ? "true" : "false")); return column; } @Override public String getDataTableName(String url) { - List urlParts = Splitter.on('/').splitToList(url); - return urlParts.get(3); - } @Override @@ -499,8 +410,8 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ @Override public CommandProcessingResult createNewDatatableEntry(final String dataTableName, final Long appTableId, final String json) { try { - final String appTable = queryForApplicationTableName(dataTableName); - CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(appTable, appTableId); + final EntityTables entityTable = queryForApplicationEntity(dataTableName); + CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(entityTable, appTableId); final List columnHeaders = this.genericDataService.fillResultsetColumnHeaders(dataTableName); @@ -509,7 +420,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ final Type typeOfMap = new TypeToken>() {}.getType(); final Map dataParams = this.fromJsonHelper.extractDataMap(typeOfMap, json); - final String sql = getAddSql(columnHeaders, dataTableName, getFKField(appTable), appTableId, dataParams); + final String sql = getAddSql(columnHeaders, dataTableName, getFKField(entityTable), appTableId, dataParams); if (!multiRow) { this.jdbcTemplate.update(sql); @@ -531,13 +442,13 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ throw new PlatformDataIntegrityException( "error.msg.datatable.entry.duplicate", "An entry already exists for datatable `" + dataTableName + "` and application table with identifier `" + appTableId + "`.", - "dataTableName", dataTableName, appTableId, dve); + API_PARAM_DATATABLE_NAME, dataTableName, appTableId, dve); } else if (realCause.getMessage().contains("doesn't have a default value") || cause.getMessage().contains("doesn't have a default value")) { throw new PlatformDataIntegrityException( "error.msg.datatable.no.value.provided.for.required.fields", "No values provided for the datatable `" + dataTableName + "` and application table with identifier `" + appTableId + "`.", - "dataTableName", dataTableName, appTableId, dve); + API_PARAM_DATATABLE_NAME, dataTableName, appTableId, dve); } logAsErrorUnexpectedDataIntegrityException(dve); @@ -549,12 +460,12 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ throw new PlatformDataIntegrityException( "error.msg.datatable.entry.duplicate", "An entry already exists for datatable `" + dataTableName + "` and application table with identifier `" + appTableId + "`.", - "dataTableName", dataTableName, appTableId, e); + API_PARAM_DATATABLE_NAME, dataTableName, appTableId, e); } else if (cause.getMessage().contains("doesn't have a default value")) { throw new PlatformDataIntegrityException( "error.msg.datatable.no.value.provided.for.required.fields", "No values provided for the datatable `" + dataTableName + "` and application table with identifier `" + appTableId + "`.", - "dataTableName", dataTableName, appTableId, e); + API_PARAM_DATATABLE_NAME, dataTableName, appTableId, e); } logAsErrorUnexpectedDataIntegrityException(e); @@ -565,17 +476,16 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ @Override public CommandProcessingResult createPPIEntry(final String dataTableName, final Long appTableId, final JsonCommand command) { - try { - final String appTable = queryForApplicationTableName(dataTableName); - final CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(appTable, appTableId); + final EntityTables entityTable = queryForApplicationEntity(dataTableName); + final CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(entityTable, appTableId); final List columnHeaders = this.genericDataService.fillResultsetColumnHeaders(dataTableName); final Type typeOfMap = new TypeToken>() {}.getType(); final Map dataParams = this.fromJsonHelper.extractDataMap(typeOfMap, command.json()); - final String sql = getAddSqlWithScore(columnHeaders, dataTableName, getFKField(appTable), appTableId, dataParams); + final String sql = getAddSqlWithScore(columnHeaders, dataTableName, getFKField(entityTable), appTableId, dataParams); this.jdbcTemplate.update(sql); @@ -588,7 +498,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ throw new PlatformDataIntegrityException( "error.msg.datatable.entry.duplicate", "An entry already exists for datatable `" + dataTableName + "` and application table with identifier `" + appTableId + "`.", - "dataTableName", dataTableName, appTableId, dve); + API_PARAM_DATATABLE_NAME, dataTableName, appTableId, dve); } logAsErrorUnexpectedDataIntegrityException(dve); @@ -600,7 +510,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ throw new PlatformDataIntegrityException( "error.msg.datatable.entry.duplicate", "An entry already exists for datatable `" + dataTableName + "` and application table with identifier `" + appTableId + "`.", - "dataTableName", dataTableName, appTableId, dve); + API_PARAM_DATATABLE_NAME, dataTableName, appTableId, dve); } logAsErrorUnexpectedDataIntegrityException(dve); @@ -609,34 +519,6 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } } - private boolean isRegisteredDataTable(final String name) { - // PERMITTED datatables - final String sql = "select (CASE WHEN exists (select 1 from x_registered_table where registered_table_name = ?) THEN 'true' ELSE 'false' END)"; - final String isRegisteredDataTable = this.jdbcTemplate.queryForObject(sql, String.class, new Object[] { name }); - return Boolean.valueOf(isRegisteredDataTable); - } - - private void assertDataTableExists(final String datatableName) { - final String sql = "select (CASE WHEN exists (select 1 from information_schema.tables where table_schema = " - + sqlGenerator.currentSchema() + " and table_name = ?) THEN 'true' ELSE 'false' END)"; - final String dataTableExistsString = this.jdbcTemplate.queryForObject(sql, String.class, new Object[] { datatableName }); // NOSONAR - final boolean dataTableExists = Boolean.valueOf(dataTableExistsString); - if (!dataTableExists) { - throw new PlatformDataIntegrityException("error.msg.invalid.datatable", "Invalid Data Table: " + datatableName, "name", - datatableName); - } - } - - private void validateDatatableName(final String name) { - - if (name == null || name.isEmpty()) { - throw new PlatformDataIntegrityException("error.msg.datatables.datatable.null.name", "Data table name must not be blank."); - } else if (!name.matches(DATATABLE_NAME_REGEX_PATTERN)) { - throw new PlatformDataIntegrityException("error.msg.datatables.datatable.invalid.name.regex", "Invalid data table name.", name); - } - SQLInjectionValidator.validateSQLInput(name); - } - private String datatableColumnNameToCodeValueName(final String columnName, final String code) { return code + "_cd_" + columnName; @@ -652,13 +534,12 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ private void parseDatatableColumnObjectForCreate(final JsonObject column, StringBuilder sqlBuilder, final StringBuilder constrainBuilder, final String dataTableNameAlias, final Map codeMappings, final boolean isConstraintApproach) { - - String name = column.has("name") ? column.get("name").getAsString() : null; - final String type = column.has("type") ? column.get("type").getAsString().toLowerCase() : null; - final Integer length = column.has("length") ? column.get("length").getAsInt() : null; - final Boolean mandatory = column.has("mandatory") ? column.get("mandatory").getAsBoolean() : false; - final Boolean unique = column.has("unique") ? column.get("unique").getAsBoolean() : false; - final String code = column.has("code") ? column.get("code").getAsString() : null; + String name = column.has(API_FIELD_NAME) ? column.get(API_FIELD_NAME).getAsString() : null; + final String type = column.has(API_FIELD_TYPE) ? column.get(API_FIELD_TYPE).getAsString().toLowerCase() : null; + final Integer length = column.has(API_FIELD_LENGTH) ? column.get(API_FIELD_LENGTH).getAsInt() : null; + final Boolean mandatory = column.has(API_FIELD_MANDATORY) ? column.get(API_FIELD_MANDATORY).getAsBoolean() : false; + final Boolean unique = column.has(API_FIELD_UNIQUE) ? column.get(API_FIELD_UNIQUE).getAsBoolean() : false; + final String code = column.has(API_FIELD_CODE) ? column.get(API_FIELD_CODE).getAsString() : null; if (StringUtils.isNotBlank(code)) { if (isConstraintApproach) { @@ -671,100 +552,76 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ name = datatableColumnNameToCodeValueName(name, code); } } - - final String dataType; - if (databaseTypeResolver.isMySQL()) { - dataType = apiTypeToMySQL.get(type); - } else if (databaseTypeResolver.isPostgreSQL()) { - dataType = apiTypeToPostgreSQL.get(type); - } else { - throw new IllegalStateException("Current database is not supported"); - } - sqlBuilder = sqlBuilder.append(sqlGenerator.escape(name) + " " + dataType); - + sqlBuilder.append(sqlGenerator.escape(name)); if (type != null) { - if (type.equalsIgnoreCase("String")) { - sqlBuilder = sqlBuilder.append("(" + length + ")"); - } else if (type.equalsIgnoreCase("Decimal")) { - sqlBuilder = sqlBuilder.append("(19,6)"); - } else if (type.equalsIgnoreCase("Dropdown")) { - if (databaseTypeResolver.isMySQL()) { - sqlBuilder = sqlBuilder.append("(11)"); - } - } + sqlBuilder.append(" ").append(mapApiTypeToDbType(type, length)); } + if (unique) { String uniqueKeyName = datatableKeywordGenerator.generateUniqueKeyName(dataTableNameAlias, name); - constrainBuilder.append(", CONSTRAINT ").append(sqlGenerator.escape(uniqueKeyName)).append(" ") - .append("UNIQUE (" + sqlGenerator.escape(name) + ")"); + constrainBuilder.append(", CONSTRAINT ").append(sqlGenerator.escape(uniqueKeyName)).append(" ").append("UNIQUE (") + .append(sqlGenerator.escape(name)).append(")"); } if (mandatory) { - sqlBuilder = sqlBuilder.append(" NOT NULL"); + sqlBuilder.append(" NOT NULL"); } else { - sqlBuilder = sqlBuilder.append(" DEFAULT NULL"); + sqlBuilder.append(" DEFAULT NULL"); } - sqlBuilder = sqlBuilder.append(", "); + sqlBuilder.append(", "); } @Transactional @Override public CommandProcessingResult createDatatable(final JsonCommand command) { - String datatableName = null; - try { this.context.authenticatedUser(); this.fromApiJsonDeserializer.validateForCreate(command.json()); final JsonElement element = this.fromJsonHelper.parse(command.json()); - final JsonArray columns = this.fromJsonHelper.extractJsonArrayNamed("columns", element); - datatableName = this.fromJsonHelper.extractStringNamed("datatableName", element); - String entitySubType = this.fromJsonHelper.extractStringNamed("entitySubType", element); - final String apptableName = this.fromJsonHelper.extractStringNamed("apptableName", element); - Boolean multiRow = this.fromJsonHelper.extractBooleanNamed("multiRow", element); + final JsonArray columns = this.fromJsonHelper.extractJsonArrayNamed(API_PARAM_COLUMNS, element); + datatableName = this.fromJsonHelper.extractStringNamed(API_PARAM_DATATABLE_NAME, element); + String entitySubType = this.fromJsonHelper.extractStringNamed(API_PARAM_SUBTYPE, element); + final String entityName = this.fromJsonHelper.extractStringNamed(API_PARAM_APPTABLE_NAME, element); + Boolean multiRow = this.fromJsonHelper.extractBooleanNamed(API_PARAM_MULTIROW, element); - /*** + /* * In cases of tables storing hierarchical entities (like m_group), different entities would end up being - * stored in the same table. - * - * Ex: Centers are a specific type of group, add abstractions for the same - ***/ - final String actualAppTableName = mapToActualAppTable(apptableName); - + * stored in the same table. Ex: Centers are a specific type of group, add abstractions for the same + */ if (multiRow == null) { multiRow = false; } validateDatatableName(datatableName); - validateAppTable(actualAppTableName); + EntityTables entityTable = resolveEntity(entityName); final boolean isConstraintApproach = this.configurationDomainService.isConstraintApproachEnabledForDatatables(); - final String fkColumnName = apptableName.substring(2) + "_id"; + final String fkColumnName = getFKField(entityTable); final String dataTableNameAlias = datatableName.toLowerCase().replaceAll("\\s", "_"); final String fkName = dataTableNameAlias + "_" + fkColumnName; StringBuilder sqlBuilder = new StringBuilder(); - final StringBuilder constrainBuilder = new StringBuilder(); - final Map codeMappings = new HashMap<>(); - sqlBuilder = sqlBuilder.append("CREATE TABLE " + sqlGenerator.escape(datatableName) + " ("); + sqlBuilder.append("CREATE TABLE " + sqlGenerator.escape(datatableName) + " ("); if (multiRow) { if (databaseTypeResolver.isMySQL()) { - sqlBuilder = sqlBuilder.append("id BIGINT NOT NULL AUTO_INCREMENT, "); + sqlBuilder.append(TABLE_FIELD_ID).append(" BIGINT NOT NULL AUTO_INCREMENT, "); } else if (databaseTypeResolver.isPostgreSQL()) { - sqlBuilder = sqlBuilder.append( - "id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ), "); + sqlBuilder.append(TABLE_FIELD_ID).append( + " bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ), "); } else { throw new IllegalStateException("Current database is not supported"); } } - sqlBuilder = sqlBuilder.append(sqlGenerator.escape(fkColumnName) + " BIGINT NOT NULL, "); + sqlBuilder.append(sqlGenerator.escape(fkColumnName) + " BIGINT NOT NULL, "); // Add Created At and Updated At - columns.add(addColumn(DataTableApiConstant.CREATEDAT_FIELD_NAME, DataTableApiConstant.DATETIME_FIELD_TYPE, false, null, false, - false)); - columns.add(addColumn(DataTableApiConstant.UPDATEDAT_FIELD_NAME, DataTableApiConstant.DATETIME_FIELD_TYPE, false, null, false, - false)); + columns.add(addColumn(DataTableApiConstant.CREATEDAT_FIELD_NAME, DATETIME, false, null, false, false)); + columns.add(addColumn(DataTableApiConstant.UPDATEDAT_FIELD_NAME, DATETIME, false, null, false, false)); + + final Map codeMappings = new HashMap<>(); + final StringBuilder constrainBuilder = new StringBuilder(); for (final JsonElement column : columns) { parseDatatableColumnObjectForCreate(column.getAsJsonObject(), sqlBuilder, constrainBuilder, dataTableNameAlias, codeMappings, isConstraintApproach); @@ -775,19 +632,21 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ String fullFkName = "fk_" + fkName; if (multiRow) { - sqlBuilder = sqlBuilder.append(", PRIMARY KEY (id)"); + sqlBuilder.append(", PRIMARY KEY (").append(TABLE_FIELD_ID).append(")"); if (databaseTypeResolver.isMySQL()) { - sqlBuilder = sqlBuilder.append(", KEY " + sqlGenerator.escape("fk_" + apptableName.substring(2) + "_id") + " (" - + sqlGenerator.escape(fkColumnName) + ")"); + sqlBuilder + .append(", KEY " + sqlGenerator.escape("fk_" + fkColumnName) + " (" + sqlGenerator.escape(fkColumnName) + ")"); } - sqlBuilder = sqlBuilder.append(", CONSTRAINT " + sqlGenerator.escape(fullFkName) + " ") + sqlBuilder.append(", CONSTRAINT " + sqlGenerator.escape(fullFkName) + " ") .append("FOREIGN KEY (" + sqlGenerator.escape(fkColumnName) + ") ") - .append("REFERENCES " + sqlGenerator.escape(actualAppTableName) + " (id)"); + .append("REFERENCES " + sqlGenerator.escape(entityTable.getApptableName()) + " (").append(TABLE_FIELD_ID) + .append(")"); } else { - sqlBuilder = sqlBuilder.append(", PRIMARY KEY (" + sqlGenerator.escape(fkColumnName) + ")") + sqlBuilder.append(", PRIMARY KEY (" + sqlGenerator.escape(fkColumnName) + ")") .append(", CONSTRAINT " + sqlGenerator.escape(fullFkName) + " ") .append("FOREIGN KEY (" + sqlGenerator.escape(fkColumnName) + ") ") - .append("REFERENCES " + sqlGenerator.escape(actualAppTableName) + " (id)"); + .append("REFERENCES " + sqlGenerator.escape(entityTable.getApptableName()) + " (").append(TABLE_FIELD_ID) + .append(")"); } sqlBuilder.append(constrainBuilder); @@ -809,7 +668,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ */ } createIndexesForTable(datatableName, columns); - registerDatatable(datatableName, apptableName, entitySubType); + registerDatatable(datatableName, entityName, entitySubType); registerColumnCodeMapping(codeMappings); } catch (final PersistenceException | DataAccessException e) { final Throwable realCause = e.getCause(); @@ -817,10 +676,10 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("datatable"); if (realCause.getMessage().toLowerCase().contains("duplicate column name")) { - baseDataValidator.reset().parameter("name").failWithCode("duplicate.column.name"); + baseDataValidator.reset().parameter(API_FIELD_NAME).failWithCode("duplicate.column.name"); } else if ((realCause.getMessage().contains("Table") || realCause.getMessage().contains("relation")) && realCause.getMessage().contains("already exists")) { - baseDataValidator.reset().parameter("datatableName").value(datatableName).failWithCode("datatable.already.exists"); + baseDataValidator.reset().parameter(API_PARAM_DATATABLE_NAME).value(datatableName).failWithCode("datatable.already.exists"); } else if (realCause.getMessage().contains("Column") && realCause.getMessage().contains("big")) { baseDataValidator.reset().parameter("column").failWithCode("length.too.big"); } else if (realCause.getMessage().contains("Row") && realCause.getMessage().contains("large")) { @@ -845,9 +704,9 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } private void createIndexForColumn(String datatableName, JsonObject column) { - String name = column.has("name") ? column.get("name").getAsString() : null; - final Boolean unique = column.has("unique") ? column.get("unique").getAsBoolean() : false; - final Boolean indexed = column.has("indexed") ? column.get("indexed").getAsBoolean() : false; + String name = column.has(API_FIELD_NAME) ? column.get(API_FIELD_NAME).getAsString() : null; + final Boolean unique = column.has(API_FIELD_UNIQUE) ? column.get(API_FIELD_UNIQUE).getAsBoolean() : false; + final Boolean indexed = column.has(API_FIELD_INDEXED) ? column.get(API_FIELD_INDEXED).getAsBoolean() : false; if (indexed) { if (!unique) { String indexName = datatableKeywordGenerator.generateIndexName(datatableName, name); @@ -862,7 +721,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ if (insertsCount == 1) { Number assignedKey = null; if (keyHolder.getKeys().size() > 1) { - assignedKey = (Long) keyHolder.getKeys().get("id"); + assignedKey = (Long) keyHolder.getKeys().get(TABLE_FIELD_ID); } else { assignedKey = keyHolder.getKey(); } @@ -878,15 +737,14 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ final Map mapColumnNameDefinition, StringBuilder sqlBuilder, final String datatableName, final StringBuilder constrainBuilder, final Map codeMappings, final List removeMappings, final boolean isConstraintApproach) { - - String name = column.has("name") ? column.get("name").getAsString() : null; - final String lengthStr = column.has("length") ? column.get("length").getAsString() : null; + String name = column.has(API_FIELD_NAME) ? column.get(API_FIELD_NAME).getAsString() : null; + final String lengthStr = column.has(API_FIELD_LENGTH) ? column.get(API_FIELD_LENGTH).getAsString() : null; Integer length = StringUtils.isNotBlank(lengthStr) ? Integer.parseInt(lengthStr) : null; - String newName = column.has("newName") ? column.get("newName").getAsString() : name; - final Boolean mandatory = column.has("mandatory") ? column.get("mandatory").getAsBoolean() : false; - final String after = column.has("after") ? column.get("after").getAsString() : null; - final String code = column.has("code") ? column.get("code").getAsString() : null; - final String newCode = column.has("newCode") ? column.get("newCode").getAsString() : null; + String newName = column.has(API_FIELD_NEWNAME) ? column.get(API_FIELD_NEWNAME).getAsString() : name; + final Boolean mandatory = column.has(API_FIELD_MANDATORY) ? column.get(API_FIELD_MANDATORY).getAsBoolean() : false; + final String after = column.has(API_FIELD_AFTER) ? column.get(API_FIELD_AFTER).getAsString() : null; + final String code = column.has(API_FIELD_CODE) ? column.get(API_FIELD_CODE).getAsString() : null; + final String newCode = column.has(API_FIELD_NEWCODE) ? column.get(API_FIELD_NEWCODE).getAsString() : null; final String dataTableNameAlias = datatableName.toLowerCase().replaceAll("\\s", "_"); if (isConstraintApproach) { if (StringUtils.isBlank(newName)) { @@ -903,7 +761,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ codeMappings.put(dataTableNameAlias + "_" + newName, (long) codeId); constrainBuilder.append(",ADD CONSTRAINT ").append(sqlGenerator.escape(newFkName)).append(" ") .append("FOREIGN KEY (" + sqlGenerator.escape(newName) + ") ").append("REFERENCES ") - .append(sqlGenerator.escape(CODE_VALUES_TABLE)).append(" (id)"); + .append(sqlGenerator.escape(CODE_VALUES_TABLE)).append(" (").append(TABLE_FIELD_ID).append(")"); } } else { @@ -918,7 +776,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ if (code == null || !StringUtils.equalsIgnoreCase(name, newName)) { constrainBuilder.append(",ADD CONSTRAINT ").append(sqlGenerator.escape(newFkName)).append(" ") .append("FOREIGN KEY (" + sqlGenerator.escape(newName) + ") ").append("REFERENCES ") - .append(sqlGenerator.escape(CODE_VALUES_TABLE)).append(" (id)"); + .append(sqlGenerator.escape(CODE_VALUES_TABLE)).append(" (").append(TABLE_FIELD_ID).append(")"); } } } @@ -937,35 +795,33 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ throw new PlatformDataIntegrityException("error.msg.datatable.column.missing.update.parse", "Column " + name + " does not exist.", name); } - final String type = mapColumnNameDefinition.get(name).getColumnType(); - if (length == null && type.toLowerCase().equals("varchar")) { - length = mapColumnNameDefinition.get(name).getColumnLength().intValue(); + final JdbcJavaType type = mapColumnNameDefinition.get(name).getColumnType(); + DatabaseType dialect = databaseTypeResolver.databaseType(); + if (length == null && type.hasPrecision(dialect)) { + Long columnLength = mapColumnNameDefinition.get(name).getColumnLength(); + length = columnLength == null ? null : columnLength.intValue(); } + if (databaseTypeResolver.isMySQL()) { - sqlBuilder = sqlBuilder.append(", CHANGE " + sqlGenerator.escape(name) + " " + sqlGenerator.escape(newName) + " " + type); + sqlBuilder.append(", CHANGE ").append(sqlGenerator.escape(name)).append(" ").append(sqlGenerator.escape(newName)).append(" ") + .append(type); } else if (databaseTypeResolver.isPostgreSQL()) { - sqlBuilder = sqlBuilder.append(", RENAME " + sqlGenerator.escape(name) + " TO " + sqlGenerator.escape(newName)); + sqlBuilder.append(", RENAME ").append(sqlGenerator.escape(name)).append(" TO ").append(sqlGenerator.escape(newName)); } if (length != null && length > 0) { - if (type.toLowerCase().equals("decimal")) { - sqlBuilder.append("(19,6)"); - } else if (type.toLowerCase().equals("varchar")) { - sqlBuilder.append("(" + length + ")"); + if (type.isDecimalType()) { + sqlBuilder.append(" ").append(type.formatSql(dialect, 19, 6)); + } else { + sqlBuilder.append(" ").append(type.formatSql(dialect, length)); } } - if (mandatory) { - if (databaseTypeResolver.isMySQL()) { - sqlBuilder = sqlBuilder.append(" NOT NULL"); - } - } else { - if (databaseTypeResolver.isMySQL()) { - sqlBuilder = sqlBuilder.append(" DEFAULT NULL"); - } + if (databaseTypeResolver.isMySQL()) { + sqlBuilder.append(mandatory ? " NOT NULL" : " DEFAULT NULL"); } if (after != null) { - sqlBuilder = sqlBuilder.append(" AFTER " + sqlGenerator.escape(after)); + sqlBuilder.append(" AFTER " + sqlGenerator.escape(after)); } } @@ -985,13 +841,13 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ private void parseDatatableColumnForAdd(final JsonObject column, StringBuilder sqlBuilder, final String dataTableNameAlias, final StringBuilder constrainBuilder, final Map codeMappings, final boolean isConstraintApproach) { - String name = column.has("name") ? column.get("name").getAsString() : null; - final String type = column.has("type") ? column.get("type").getAsString().toLowerCase() : null; - final Integer length = column.has("length") ? column.get("length").getAsInt() : null; - final Boolean mandatory = column.has("mandatory") ? column.get("mandatory").getAsBoolean() : false; - final Boolean unique = column.has("unique") ? column.get("unique").getAsBoolean() : false; - final String after = column.has("after") ? column.get("after").getAsString() : null; - final String code = column.has("code") ? column.get("code").getAsString() : null; + String name = column.has(API_FIELD_NAME) ? column.get(API_FIELD_NAME).getAsString() : null; + final String type = column.has(API_FIELD_TYPE) ? column.get(API_FIELD_TYPE).getAsString().toLowerCase() : null; + final Integer length = column.has(API_FIELD_LENGTH) ? column.get(API_FIELD_LENGTH).getAsInt() : null; + final Boolean mandatory = column.has(API_FIELD_MANDATORY) ? column.get(API_FIELD_MANDATORY).getAsBoolean() : false; + final Boolean unique = column.has(API_FIELD_UNIQUE) ? column.get(API_FIELD_UNIQUE).getAsBoolean() : false; + final String after = column.has(API_FIELD_AFTER) ? column.get(API_FIELD_AFTER).getAsString() : null; + final String code = column.has(API_FIELD_CODE) ? column.get(API_FIELD_CODE).getAsString() : null; if (StringUtils.isNotBlank(code)) { if (isConstraintApproach) { @@ -999,31 +855,12 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ codeMappings.put(dataTableNameAlias + "_" + name, this.codeReadPlatformService.retriveCode(code).getId()); constrainBuilder.append(",ADD CONSTRAINT ").append(sqlGenerator.escape(fkName)).append(" ") .append("FOREIGN KEY (" + sqlGenerator.escape(name) + ") ").append("REFERENCES ") - .append(sqlGenerator.escape(CODE_VALUES_TABLE)).append(" (id)"); + .append(sqlGenerator.escape(CODE_VALUES_TABLE)).append(" (").append(TABLE_FIELD_ID).append(")"); } else { name = datatableColumnNameToCodeValueName(name, code); } } - - final String dataType; - if (databaseTypeResolver.isMySQL()) { - dataType = apiTypeToMySQL.get(type); - } else if (databaseTypeResolver.isPostgreSQL()) { - dataType = apiTypeToPostgreSQL.get(type); - } else { - throw new IllegalStateException("Current database is not supported"); - } - sqlBuilder = sqlBuilder.append(", ADD " + sqlGenerator.escape(name) + " " + dataType); - - if (type != null) { - if (type.equalsIgnoreCase("String") && length != null) { - sqlBuilder = sqlBuilder.append("(" + length + ")"); - } else if (type.equalsIgnoreCase("Decimal")) { - sqlBuilder = sqlBuilder.append("(19,6)"); - } else if (type.equalsIgnoreCase("Dropdown")) { - sqlBuilder = sqlBuilder.append("(11)"); - } - } + sqlBuilder.append(", ADD ").append(sqlGenerator.escape(name)).append(" ").append(mapApiTypeToDbType(type, length)); if (unique) { String uniqueKeyName = datatableKeywordGenerator.generateUniqueKeyName(dataTableNameAlias, name); @@ -1032,20 +869,20 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } if (mandatory) { - sqlBuilder = sqlBuilder.append(" NOT NULL"); + sqlBuilder.append(" NOT NULL"); } else { - sqlBuilder = sqlBuilder.append(" DEFAULT NULL"); + sqlBuilder.append(" DEFAULT NULL"); } if (after != null) { - sqlBuilder = sqlBuilder.append(" AFTER " + sqlGenerator.escape(after)); + sqlBuilder.append(" AFTER " + sqlGenerator.escape(after)); } } private void parseDatatableColumnForDrop(final JsonObject column, StringBuilder sqlBuilder, final String datatableName, final StringBuilder constrainBuilder, final List codeMappings) { final String datatableAlias = datatableName.toLowerCase().replaceAll("\\s", "_"); - final String name = column.has("name") ? column.get("name").getAsString() : null; + final String name = column.has(API_FIELD_NAME) ? column.get(API_FIELD_NAME).getAsString() : null; final StringBuilder findFKSql = new StringBuilder(); findFKSql.append("SELECT count(*)").append("FROM information_schema.TABLE_CONSTRAINTS i") .append(" WHERE i.CONSTRAINT_TYPE = 'FOREIGN KEY'").append(" AND i.TABLE_SCHEMA = DATABASE()") @@ -1082,7 +919,6 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ this.jdbcTemplate.batchUpdate(deleteSqlList); } - } /** @@ -1097,43 +933,35 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ **/ private void removeNullValuesFromStringColumn(final String datatableName, final JsonObject column, final Map mapColumnNameDefinition) { - final Boolean mandatory = column.has("mandatory") ? column.get("mandatory").getAsBoolean() : false; - final String name = column.has("name") ? column.get("name").getAsString() : ""; - final String type = mapColumnNameDefinition.containsKey(name) ? mapColumnNameDefinition.get(name).getColumnType() : ""; + final boolean mandatory = column.has(API_FIELD_MANDATORY) && column.get(API_FIELD_MANDATORY).getAsBoolean(); + final String name = column.has(API_FIELD_NAME) ? column.get(API_FIELD_NAME).getAsString() : ""; + final JdbcJavaType type = mapColumnNameDefinition.containsKey(name) ? mapColumnNameDefinition.get(name).getColumnType() : null; - if (StringUtils.isNotEmpty(type)) { - if (mandatory && stringDataTypes.contains(type.toLowerCase())) { - StringBuilder sqlBuilder = new StringBuilder(); - sqlBuilder.append("UPDATE " + sqlGenerator.escape(datatableName) + " SET " + sqlGenerator.escape(name) + " = '' WHERE " - + sqlGenerator.escape(name) + " IS NULL"); - - this.jdbcTemplate.update(sqlBuilder.toString()); - } + if (type != null && mandatory && type.isStringType()) { + String sql = "UPDATE " + sqlGenerator.escape(datatableName) + " SET " + sqlGenerator.escape(name) + " = '' WHERE " + + sqlGenerator.escape(name) + " IS NULL"; + this.jdbcTemplate.update(sql); } } @Transactional @Override public void updateDatatable(final String datatableName, final JsonCommand command) { - try { this.context.authenticatedUser(); this.fromApiJsonDeserializer.validateForUpdate(command.json()); final JsonElement element = this.fromJsonHelper.parse(command.json()); - final JsonArray changeColumns = this.fromJsonHelper.extractJsonArrayNamed("changeColumns", element); - final JsonArray addColumns = this.fromJsonHelper.extractJsonArrayNamed("addColumns", element); - final JsonArray dropColumns = this.fromJsonHelper.extractJsonArrayNamed("dropColumns", element); - final String apptableName = this.fromJsonHelper.extractStringNamed("apptableName", element); - final String entitySubType = this.fromJsonHelper.extractStringNamed("entitySubType", element); + final JsonArray changeColumns = this.fromJsonHelper.extractJsonArrayNamed(API_PARAM_CHANGECOLUMNS, element); + final JsonArray addColumns = this.fromJsonHelper.extractJsonArrayNamed(API_PARAM_ADDCOLUMNS, element); + final JsonArray dropColumns = this.fromJsonHelper.extractJsonArrayNamed(API_PARAM_DROPCOLUMNS, element); + final String entityName = this.fromJsonHelper.extractStringNamed(API_PARAM_APPTABLE_NAME, element); + final String entitySubType = this.fromJsonHelper.extractStringNamed(API_PARAM_SUBTYPE, element); validateDatatableName(datatableName); int rowCount = getRowCount(datatableName); final List columnHeaderData = this.genericDataService.fillResultsetColumnHeaders(datatableName); - final Map mapColumnNameDefinition = new HashMap<>(); - for (final ResultsetColumnHeaderData columnHeader : columnHeaderData) { - mapColumnNameDefinition.put(columnHeader.getColumnName(), columnHeader); - } + final Map mapColumnNameDefinition = SearchUtil.mapHeadersToName(columnHeaderData); final boolean isConstraintApproach = this.configurationDomainService.isConstraintApproachEnabledForDatatables(); @@ -1142,14 +970,12 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ new Object[] { entitySubType, datatableName }); } - if (!StringUtils.isBlank(apptableName)) { - validateAppTable(apptableName); - - final String oldApptableName = queryForApplicationTableName(datatableName); - if (!StringUtils.equals(oldApptableName, apptableName)) { - final String oldFKName = oldApptableName.substring(2) + "_id"; - final String newFKName = apptableName.substring(2) + "_id"; - final String actualAppTableName = mapToActualAppTable(apptableName); + if (!StringUtils.isBlank(entityName)) { + EntityTables entityTable = resolveEntity(entityName); + EntityTables oldEntityTable = queryForApplicationEntity(datatableName); + if (entityTable != oldEntityTable) { + final String oldFKName = getFKField(oldEntityTable); + final String newFKName = getFKField(entityTable); final String oldConstraintName = datatableName.toLowerCase().replaceAll("\\s", "_") + "_" + oldFKName; final String newConstraintName = datatableName.toLowerCase().replaceAll("\\s", "_") + "_" + newFKName; StringBuilder sqlBuilder = new StringBuilder(); @@ -1158,8 +984,8 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ String fullOldConstraint = "fk_" + oldConstraintName; String fullNewFk = "fk_" + newFKName; String fullNewConstraint = "fk_" + newConstraintName; - if (mapColumnNameDefinition.containsKey("id")) { - sqlBuilder = sqlBuilder.append("ALTER TABLE " + sqlGenerator.escape(datatableName) + " ") + if (mapColumnNameDefinition.containsKey(TABLE_FIELD_ID)) { + sqlBuilder.append("ALTER TABLE " + sqlGenerator.escape(datatableName) + " ") .append("DROP KEY " + sqlGenerator.escape(fullOldFk) + ",") .append("DROP FOREIGN KEY " + sqlGenerator.escape(fullOldConstraint) + ",") .append("CHANGE COLUMN " + sqlGenerator.escape(oldFKName) + " " + sqlGenerator.escape(newFKName) @@ -1167,21 +993,23 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ .append("ADD KEY " + sqlGenerator.escape(fullNewFk) + " (" + sqlGenerator.escape(newFKName) + "),") .append("ADD CONSTRAINT " + sqlGenerator.escape(fullNewConstraint) + " ") .append("FOREIGN KEY (" + sqlGenerator.escape(newFKName) + ") ") - .append("REFERENCES " + sqlGenerator.escape(actualAppTableName) + " (id)"); + .append("REFERENCES " + sqlGenerator.escape(entityTable.getApptableName()) + " (").append(TABLE_FIELD_ID) + .append(")"); } else { - sqlBuilder = sqlBuilder.append("ALTER TABLE " + sqlGenerator.escape(datatableName) + " ") + sqlBuilder.append("ALTER TABLE " + sqlGenerator.escape(datatableName) + " ") .append("DROP FOREIGN KEY " + sqlGenerator.escape(fullOldConstraint) + ",") .append("CHANGE COLUMN " + sqlGenerator.escape(oldFKName) + " " + sqlGenerator.escape(newFKName) + " BIGINT NOT NULL,") .append("ADD CONSTRAINT " + sqlGenerator.escape(fullNewConstraint) + " ") .append("FOREIGN KEY (" + sqlGenerator.escape(newFKName) + ") ") - .append("REFERENCES " + sqlGenerator.escape(actualAppTableName) + " (id)"); + .append("REFERENCES " + sqlGenerator.escape(entityTable.getApptableName()) + " (").append(TABLE_FIELD_ID) + .append(")"); } this.jdbcTemplate.execute(sqlBuilder.toString()); deregisterDatatable(datatableName); - registerDatatable(datatableName, apptableName, entitySubType); + registerDatatable(datatableName, entityName, entitySubType); } } @@ -1217,7 +1045,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ final Map codeMappings = new HashMap<>(); for (final JsonElement column : addColumns) { JsonObject columnAsJson = column.getAsJsonObject(); - if (rowCount > 0 && columnAsJson.has("mandatory") && columnAsJson.get("mandatory").getAsBoolean()) { + if (rowCount > 0 && columnAsJson.has(API_FIELD_MANDATORY) && columnAsJson.get(API_FIELD_MANDATORY).getAsBoolean()) { throw new GeneralPlatformDomainRuleException("error.msg.non.empty.datatable.mandatory.column.cannot.be.added", "Non empty datatable mandatory columns can not be added."); } @@ -1236,7 +1064,6 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ registerColumnCodeMapping(codeMappings); } if (changeColumns != null) { - StringBuilder sqlBuilder = new StringBuilder("ALTER TABLE " + sqlGenerator.escape(datatableName)); final StringBuilder constrainBuilder = new StringBuilder(); final Map codeMappings = new HashMap<>(); @@ -1287,11 +1114,11 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("datatable"); if (realCause.getMessage().toLowerCase().contains("unknown column")) { - baseDataValidator.reset().parameter("name").failWithCode("does.not.exist"); + baseDataValidator.reset().parameter(API_FIELD_NAME).failWithCode("does.not.exist"); } else if (realCause.getMessage().toLowerCase().contains("can't drop")) { - baseDataValidator.reset().parameter("name").failWithCode("does.not.exist"); + baseDataValidator.reset().parameter(API_FIELD_NAME).failWithCode("does.not.exist"); } else if (realCause.getMessage().toLowerCase().contains("duplicate column")) { - baseDataValidator.reset().parameter("name").failWithCode("column.already.exists"); + baseDataValidator.reset().parameter(API_FIELD_NAME).failWithCode("column.already.exists"); } throwExceptionIfValidationWarningsExist(dataValidationErrors); @@ -1300,10 +1127,10 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ final List dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("datatable"); if (realCause.getMessage().toLowerCase().contains("duplicate column name")) { - baseDataValidator.reset().parameter("name").failWithCode("duplicate.column.name"); + baseDataValidator.reset().parameter(API_FIELD_NAME).failWithCode("duplicate.column.name"); } else if ((realCause.getMessage().contains("Table") || realCause.getMessage().contains("relation")) && realCause.getMessage().contains("already exists")) { - baseDataValidator.reset().parameter("datatableName").value(datatableName).failWithCode("datatable.already.exists"); + baseDataValidator.reset().parameter(API_PARAM_DATATABLE_NAME).value(datatableName).failWithCode("datatable.already.exists"); } else if (realCause.getMessage().contains("Column") && realCause.getMessage().contains("big")) { baseDataValidator.reset().parameter("column").failWithCode("length.too.big"); } else if (realCause.getMessage().contains("Row") && realCause.getMessage().contains("large")) { @@ -1317,8 +1144,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ private void updateUniqueConstraintsForTable(String datatableName, JsonArray changeColumns, Map mapColumnNameDefinition) { for (final JsonElement column : changeColumns) { - - String name = column.getAsJsonObject().has("name") ? column.getAsJsonObject().get("name").getAsString() : null; + String name = column.getAsJsonObject().has(API_FIELD_NAME) ? column.getAsJsonObject().get(API_FIELD_NAME).getAsString() : null; if (!mapColumnNameDefinition.containsKey(name)) { throw new PlatformDataIntegrityException("error.msg.datatable.column.missing.update.parse", @@ -1326,15 +1152,15 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } updateColumnUniqueConstraints(datatableName, column.getAsJsonObject(), - mapColumnNameDefinition.get(column.getAsJsonObject().get("name").getAsString())); + mapColumnNameDefinition.get(column.getAsJsonObject().get(API_FIELD_NAME).getAsString())); } } private void updateColumnUniqueConstraints(String datatableName, JsonObject column, ResultsetColumnHeaderData columnMetaData) { // check for unique constraint update - String name = column.has("name") ? column.get("name").getAsString() : null; - String columnNewName = column.has("newName") ? column.get("newName").getAsString() : null; - final Boolean setUnique = column.has("unique") ? column.get("unique").getAsBoolean() : false; + String name = column.has(API_FIELD_NAME) ? column.get(API_FIELD_NAME).getAsString() : null; + String columnNewName = column.has(API_FIELD_NEWNAME) ? column.get(API_FIELD_NEWNAME).getAsString() : null; + final Boolean setUnique = column.has(API_FIELD_UNIQUE) ? column.get(API_FIELD_UNIQUE).getAsBoolean() : false; final Boolean isAlreadyUnique = genericDataService.isExplicitlyUnique(datatableName, name); String uniqueKeyName = datatableKeywordGenerator.generateUniqueKeyName(datatableName, name); @@ -1391,21 +1217,21 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ private void updateIndexesForTable(String datatableName, JsonArray changeColumns, Map mapColumnNameDefinition) { for (final JsonElement column : changeColumns) { - String name = column.getAsJsonObject().has("name") ? column.getAsJsonObject().get("name").getAsString() : null; + String name = column.getAsJsonObject().has(API_FIELD_NAME) ? column.getAsJsonObject().get(API_FIELD_NAME).getAsString() : null; if (!mapColumnNameDefinition.containsKey(name)) { throw new PlatformDataIntegrityException("error.msg.datatable.column.missing.update.parse", "Column " + name + " does not exist.", name); } updateIndexForColumn(datatableName, column.getAsJsonObject(), - mapColumnNameDefinition.get(column.getAsJsonObject().get("name").getAsString())); + mapColumnNameDefinition.get(column.getAsJsonObject().get(API_FIELD_NAME).getAsString())); } } private void updateIndexForColumn(String datatableName, JsonObject column, ResultsetColumnHeaderData columnMetaData) { - String name = column.has("name") ? column.get("name").getAsString() : null; - String columnNewName = column.has("newName") ? column.get("newName").getAsString() : null; - final Boolean setForUnique = column.has("unique") ? column.get("unique").getAsBoolean() : false; - final Boolean setForIndexed = column.has("indexed") ? column.get("indexed").getAsBoolean() : false; + String name = column.has(API_FIELD_NAME) ? column.get(API_FIELD_NAME).getAsString() : null; + String columnNewName = column.has(API_FIELD_NEWNAME) ? column.get(API_FIELD_NEWNAME).getAsString() : null; + final Boolean setForUnique = column.has(API_FIELD_UNIQUE) ? column.get(API_FIELD_UNIQUE).getAsBoolean() : false; + final Boolean setForIndexed = column.has(API_FIELD_INDEXED) ? column.get(API_FIELD_INDEXED).getAsBoolean() : false; if (!setForUnique) { final Boolean isAlreadyIndexed = genericDataService.isExplicitlyIndexed(datatableName, name); String indexName = datatableKeywordGenerator.generateIndexName(datatableName, name); @@ -1422,9 +1248,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ checkColumnRenameAndCreateIndex(datatableName, name, columnNewName, indexName); } } - } - } private void checkColumnRenameAndCreateIndex(String datatableName, String columnExistingName, String columnNewName, String indexName) { @@ -1468,13 +1292,9 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ @Transactional @Override public void deleteDatatable(final String datatableName) { - try { this.context.authenticatedUser(); - if (!isRegisteredDataTable(datatableName)) { - throw new DatatableNotFoundException(datatableName); - } - validateDatatableName(datatableName); + validateDatatableRegistered(datatableName); assertDataTableEmpty(datatableName); deregisterDatatable(datatableName); String[] sqlArray = null; @@ -1494,7 +1314,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ final List dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("datatable"); if (realCause.getMessage().contains("Unknown table")) { - baseDataValidator.reset().parameter("datatableName").failWithCode("does.not.exist"); + baseDataValidator.reset().parameter(API_PARAM_DATATABLE_NAME).failWithCode("does.not.exist"); } throwExceptionIfValidationWarningsExist(dataValidationErrors); @@ -1518,7 +1338,6 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ @Override public CommandProcessingResult updateDatatableEntryOneToOne(final String dataTableName, final Long appTableId, final JsonCommand command) { - return updateDatatableEntry(dataTableName, appTableId, null, command); } @@ -1526,17 +1345,15 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ @Override public CommandProcessingResult updateDatatableEntryOneToMany(final String dataTableName, final Long appTableId, final Long datatableId, final JsonCommand command) { - return updateDatatableEntry(dataTableName, appTableId, datatableId, command); } private CommandProcessingResult updateDatatableEntry(final String dataTableName, final Long appTableId, final Long datatableId, final JsonCommand command) { + final EntityTables entityTable = queryForApplicationEntity(dataTableName); + final CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(entityTable, appTableId); - final String appTable = queryForApplicationTableName(dataTableName); - final CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(appTable, appTableId); - - final GenericResultsetData grs = retrieveDataTableGenericResultSetForUpdate(appTable, dataTableName, appTableId, datatableId); + final GenericResultsetData grs = retrieveDataTableGenericResultSetForUpdate(entityTable, dataTableName, appTableId, datatableId); if (grs.hasNoEntries()) { throw new DatatableNotFoundException(dataTableName, appTableId); @@ -1550,9 +1367,9 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ final Type typeOfMap = new TypeToken>() {}.getType(); final Map dataParams = this.fromJsonHelper.extractDataMap(typeOfMap, command.json()); - String pkName = "id"; // 1:M datatable + String pkName = TABLE_FIELD_ID; // 1:M datatable if (datatableId == null) { - pkName = getFKField(appTable); + pkName = getFKField(entityTable); } // 1:1 datatable final Map changes = getAffectedAndChangedColumns(grs, dataParams, pkName); @@ -1584,14 +1401,13 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ @Transactional @Override public CommandProcessingResult deleteDatatableEntries(final String dataTableName, final Long appTableId) { - validateDatatableName(dataTableName); if (isDatatableAttachedToEntityDatatableCheck(dataTableName)) { throw new DatatableEntryRequiredException(dataTableName, appTableId); } - final String appTable = queryForApplicationTableName(dataTableName); - final CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(appTable, appTableId); - final String deleteOneToOneEntrySql = getDeleteEntriesSql(dataTableName, getFKField(appTable), appTableId); + final EntityTables entityTable = queryForApplicationEntity(dataTableName); + final CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(entityTable, appTableId); + final String deleteOneToOneEntrySql = getDeleteEntriesSql(dataTableName, getFKField(entityTable), appTableId); final int rowsDeleted = this.jdbcTemplate.update(deleteOneToOneEntrySql); if (rowsDeleted < 1) { @@ -1608,8 +1424,8 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ if (isDatatableAttachedToEntityDatatableCheck(dataTableName)) { throw new DatatableEntryRequiredException(dataTableName, appTableId); } - final String appTable = queryForApplicationTableName(dataTableName); - final CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(appTable, appTableId); + final EntityTables entityTable = queryForApplicationEntity(dataTableName); + final CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(entityTable, appTableId); final String sql = getDeleteEntrySql(dataTableName, datatableId); @@ -1621,23 +1437,20 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ @Transactional(readOnly = true) public GenericResultsetData retrieveDataTableGenericResultSet(final String dataTableName, final Long appTableId, final String order, final Long id) { - - final String appTable = queryForApplicationTableName(dataTableName); - - checkMainResourceExistsWithinScope(appTable, appTableId); + final EntityTables entityTable = queryForApplicationEntity(dataTableName); + checkMainResourceExistsWithinScope(entityTable, appTableId); final List columnHeaders = this.genericDataService.fillResultsetColumnHeaders(dataTableName); - final boolean multiRow = isMultirowDatatable(columnHeaders); - String whereClause = getFKField(appTable) + " = " + appTableId; + String whereClause = getFKField(entityTable) + " = " + appTableId; SQLInjectionValidator.validateSQLInput(whereClause); String sql = "select * from " + sqlGenerator.escape(dataTableName) + " where " + whereClause; // id only used for reading a specific entry that belongs to appTableId (in a // one to many datatable) if (multiRow && id != null) { - sql = sql + " and id = " + id; + sql = sql + " and " + TABLE_FIELD_ID + " = " + id; } if (StringUtils.isNotBlank(order)) { @@ -1645,41 +1458,38 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ sql = sql + " order by " + order; } - final List result = fillDatatableResultSetDataRows(sql, columnHeaders); + final List result = genericDataService.fillResultsetRowData(sql, columnHeaders); return new GenericResultsetData(columnHeaders, result); } - private GenericResultsetData retrieveDataTableGenericResultSetForUpdate(final String appTable, final String dataTableName, + private GenericResultsetData retrieveDataTableGenericResultSetForUpdate(final EntityTables entityTable, final String dataTableName, final Long appTableId, final Long id) { - final List columnHeaders = this.genericDataService.fillResultsetColumnHeaders(dataTableName); final boolean multiRow = isMultirowDatatable(columnHeaders); - String whereClause = getFKField(appTable) + " = " + appTableId; + String whereClause = getFKField(entityTable) + " = " + appTableId; SQLInjectionValidator.validateSQLInput(whereClause); String sql = "select * from " + sqlGenerator.escape(dataTableName) + " where " + whereClause; // id only used for reading a specific entry that belongs to appTableId (in a // one to many datatable) if (multiRow && id != null) { - sql = sql + " and id = " + id; + sql = sql + " and " + TABLE_FIELD_ID + " = " + id; } - final List result = fillDatatableResultSetDataRows(sql, columnHeaders); - + final List result = genericDataService.fillResultsetRowData(sql, columnHeaders); return new GenericResultsetData(columnHeaders, result); } - private CommandProcessingResult checkMainResourceExistsWithinScope(final String appTable, final Long appTableId) { - - final String sql = dataScopedSQL(appTable, appTableId); + private CommandProcessingResult checkMainResourceExistsWithinScope(@NotNull EntityTables entityTable, final Long appTableId) { + final String sql = dataScopedSQL(entityTable, appTableId); log.debug("data scoped sql: {}", sql); final SqlRowSet rs = this.jdbcTemplate.queryForRowSet(sql); if (!rs.next()) { - throw new DatatableNotFoundException(appTable, appTableId); + throw new DatatableNotFoundException(entityTable, appTableId); } final Long officeId = getLongSqlRowSet(rs, "officeId"); @@ -1710,171 +1520,82 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ return val; } - private String dataScopedSQL(final String appTable, final Long appTableId) { + private String dataScopedSQL(@NotNull EntityTables entityTable, final Long appTableId) { /* * unfortunately have to, one way or another, be able to restrict data to the users office hierarchy. Here, a * few key tables are done. But if additional fields are needed on other tables the same pattern applies */ + final AppUser currentUser = this.context.authenticatedUser(); - String currentUserOfficeHierarchy = currentUser.getOffice().getHierarchy(); - String officeHierarchyCondition = " o.hierarchy like '" + currentUserOfficeHierarchy + "%'"; - String scopedSQL = null; + String officeHierarchy = currentUser.getOffice().getHierarchy(); /* * m_loan and m_savings_account are connected to an m_office thru either an m_client or an m_group If both it * means it relates to an m_client that is in a group (still an m_client account) */ - EntityTables entityTable = EntityTables.fromName(appTable); - switch (entityTable) { - case LOAN: - scopedSQL = "select distinct x.* from (" - + " (select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId, null as entityId from m_loan l " - + getClientOfficeJoinCondition(currentUserOfficeHierarchy, appTableId, "l") + " where l.id = " + appTableId + ")" - + " union all " - + " (select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId, null as entityId from m_loan l " - + getGroupOfficeJoinCondition(currentUserOfficeHierarchy, appTableId, "l") + " where l.id = " + appTableId + ")" - + " ) as x"; - break; - case SAVING: - scopedSQL = "select distinct x.* from (" - + " (select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, null as entityId from m_savings_account s " - + getClientOfficeJoinCondition(currentUserOfficeHierarchy, appTableId, "s") + " where s.id = " + appTableId + ")" - + " union all " - + " (select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, null as entityId from m_savings_account s " - + getGroupOfficeJoinCondition(currentUserOfficeHierarchy, appTableId, "s") + " where s.id = " + appTableId + ")" - + " ) as x"; - break; - case SAVINGS_TRANSACTION: - scopedSQL = "select distinct x.* from (" - + " (select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, t.id as entityId from m_savings_account_transaction t" - + " join m_savings_account s on t.savings_account_id = s.id " - + getClientOfficeJoinCondition(currentUserOfficeHierarchy, appTableId, "s") + " where t.id = " + appTableId + ")" - + " union all " - + " (select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, t.id as entityId from m_savings_account_transaction t " - + " join m_savings_account s on t.savings_account_id = s.id " - + getGroupOfficeJoinCondition(currentUserOfficeHierarchy, appTableId, "s") + " where t.id = " + appTableId + ")" - + " ) as x"; - break; - case CLIENT: - scopedSQL = "select o.id as officeId, null as groupId, c.id as clientId, null as savingsId, null as loanId, null as entityId from m_client c " - + getOfficeJoinCondition(currentUserOfficeHierarchy, appTableId, "c") + " where c.id = " + appTableId; - break; - case GROUP: - scopedSQL = "select o.id as officeId, g.id as groupId, null as clientId, null as savingsId, null as loanId, null as entityId from m_group g " - + getOfficeJoinCondition(currentUserOfficeHierarchy, appTableId, "g") + " where g.id = " + appTableId; - break; - case OFFICE: - scopedSQL = "select o.id as officeId, null as groupId, null as clientId, null as savingsId, null as loanId, null as entityId from m_office o " - + " where " + officeHierarchyCondition + " and o.id = " + appTableId; - break; - case PRODUCT_LOAN: - case SAVINGS_PRODUCT: - case SHARE_PRODUCT: - scopedSQL = "select null as officeId, null as groupId, null as clientId, null as savingsId, null as loanId, p.id as entityId from " - + appTable + " as p WHERE p.id = " + appTableId; - break; - default: - throw new PlatformDataIntegrityException("error.msg.invalid.dataScopeCriteria", - "Application Table: " + appTable + " not catered for in data Scoping"); - } - return scopedSQL; - + return switch (entityTable) { + case LOAN -> "select distinct x.* from (" + + " (select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId, null as entityId from m_loan l " + + getClientOfficeJoinCondition(officeHierarchy, "l") + " where l.id = " + appTableId + ")" + " union all " + + " (select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId, null as entityId from m_loan l " + + getGroupOfficeJoinCondition(officeHierarchy, "l") + " where l.id = " + appTableId + ")" + " ) as x"; + case SAVINGS -> "select distinct x.* from (" + + " (select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, null as entityId from m_savings_account s " + + getClientOfficeJoinCondition(officeHierarchy, "s") + " where s.id = " + appTableId + ")" + " union all " + + " (select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, null as entityId from m_savings_account s " + + getGroupOfficeJoinCondition(officeHierarchy, "s") + " where s.id = " + appTableId + ")" + " ) as x"; + case SAVINGS_TRANSACTION -> "select distinct x.* from (" + + " (select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, t.id as entityId from m_savings_account_transaction t" + + " join m_savings_account s on t.savings_account_id = s.id " + getClientOfficeJoinCondition(officeHierarchy, "s") + + " where t.id = " + appTableId + ")" + " union all " + + " (select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, t.id as entityId from m_savings_account_transaction t " + + " join m_savings_account s on t.savings_account_id = s.id " + getGroupOfficeJoinCondition(officeHierarchy, "s") + + " where t.id = " + appTableId + ")" + " ) as x"; + case CLIENT -> + "select o.id as officeId, null as groupId, c.id as clientId, null as savingsId, null as loanId, null as entityId from m_client c " + + getOfficeJoinCondition(officeHierarchy, "c") + " where c.id = " + appTableId; + case GROUP, CENTER -> + "select o.id as officeId, g.id as groupId, null as clientId, null as savingsId, null as loanId, null as entityId from m_group g " + + getOfficeJoinCondition(officeHierarchy, "g") + " where g.id = " + appTableId; + case OFFICE -> + "select o.id as officeId, null as groupId, null as clientId, null as savingsId, null as loanId, null as entityId from m_office o " + + " where o.hierarchy like '" + officeHierarchy + "%'" + " and o.id = " + appTableId; + case LOAN_PRODUCT, SAVINGS_PRODUCT, SHARE_PRODUCT -> + "select null as officeId, null as groupId, null as clientId, null as savingsId, null as loanId, p.id as entityId from " + + entityTable.getName() + " as p WHERE p.id = " + appTableId; + default -> throw new PlatformDataIntegrityException("error.msg.invalid.dataScopeCriteria", + "Application Table: " + entityTable.getName() + " not catered for in data Scoping"); + }; } - private String getClientOfficeJoinCondition(String currentUserOfficeHierarchy, Long appTableId, String appTableAlias) { - StringBuilder clientOfficeJoinBuilder = new StringBuilder(); - clientOfficeJoinBuilder.append(" join m_client c on c.id = ").append(appTableAlias).append(".client_id ") - .append(getOfficeJoinCondition(currentUserOfficeHierarchy, appTableId, "c")); - return clientOfficeJoinBuilder.toString(); + private String getClientOfficeJoinCondition(String officeHierarchy, String appTableAlias) { + return " join m_client c on c.id = " + appTableAlias + ".client_id " + getOfficeJoinCondition(officeHierarchy, "c"); } - private String getGroupOfficeJoinCondition(String currentUserOfficeHierarchy, Long appTableId, String appTableAlias) { - StringBuilder clientOfficeJoinBuilder = new StringBuilder(); - clientOfficeJoinBuilder.append(" join m_group g on g.id = ").append(appTableAlias).append(".client_id ") - .append(getOfficeJoinCondition(currentUserOfficeHierarchy, appTableId, "g")); - return clientOfficeJoinBuilder.toString(); + private String getGroupOfficeJoinCondition(String officeHierarchy, String appTableAlias) { + return " join m_group g on g.id = " + appTableAlias + ".client_id " + getOfficeJoinCondition(officeHierarchy, "g"); } - private String getOfficeJoinCondition(String currentUserOfficeHierarchy, Long appTableId, String joiningTableAlias) { - StringBuilder officeJoinBuilder = new StringBuilder(); - officeJoinBuilder.append(" join m_office o on o.id = ").append(joiningTableAlias).append(".office_id and ") - .append(" o.hierarchy like '").append(currentUserOfficeHierarchy).append("%' "); - return officeJoinBuilder.toString(); + private String getOfficeJoinCondition(String officeHierarchy, String joinTableAlias) { + return " join m_office o on o.id = " + joinTableAlias + ".office_id and o.hierarchy like '" + officeHierarchy + "%' "; } - private void validateAppTable(final String appTable) { - if (EntityTables.fromName(appTable) == null) { - throw new PlatformDataIntegrityException("error.msg.invalid.application.table", "Invalid Application Table: " + appTable, - "name", appTable); - } - } - - private String mapToActualAppTable(final String appTable) { - if ("m_center".equalsIgnoreCase(appTable)) { - return EntityTables.GROUP.getName(); - } - return appTable; - } - - private List fillDatatableResultSetDataRows(final String sql, final List columnHeaders) { - final List resultsetDataRows = new ArrayList<>(); - final GenericResultsetData genericResultsetData = new GenericResultsetData(columnHeaders, null); - - final SqlRowSet rowSet = jdbcTemplate.queryForRowSet(sql); // NOSONAR - final SqlRowSetMetaData rsmd = rowSet.getMetaData(); - - while (rowSet.next()) { - final List columnValues = new ArrayList<>(); - - for (int i = 0; i < rsmd.getColumnCount(); i++) { - final String columnName = rsmd.getColumnName(i + 1); - final String colType = genericResultsetData.getColTypeOfColumnNamed(columnName); - - if ("DATE".equalsIgnoreCase(colType)) { - Date tmpDate = (Date) rowSet.getObject(columnName); - columnValues.add(tmpDate != null ? tmpDate.toLocalDate() : null); - } else if ("TIMESTAMP WITHOUT TIME ZONE".equalsIgnoreCase(colType) // PostgreSQL - || "DATETIME".equalsIgnoreCase(colType) || "TIMESTAMP".equalsIgnoreCase(colType)) { - Object tmpDate = rowSet.getObject(columnName); - - columnValues - .add(tmpDate != null ? tmpDate instanceof Timestamp ? ((Timestamp) tmpDate).toLocalDateTime() : tmpDate : null); - } else { - columnValues.add(rowSet.getObject(columnName)); - } - } - - resultsetDataRows.add(ResultsetRowData.create(columnValues)); - } - - return resultsetDataRows; - } - - private String queryForApplicationTableName(final String datatable) { + @NotNull + private EntityTables queryForApplicationEntity(final String datatable) { SQLInjectionValidator.validateSQLInput(datatable); final String sql = "SELECT application_table_name FROM x_registered_table where registered_table_name = ?"; - - String applicationTableName = ""; - final SqlRowSet rowSet = jdbcTemplate.queryForRowSet(sql, datatable); // NOSONAR + + String applicationTableName; if (rowSet.next()) { applicationTableName = rowSet.getString("application_table_name"); } else { throw new DatatableNotFoundException(datatable); } - - return applicationTableName; + return resolveEntity(applicationTableName); } - private void validateDatatable(final String datatable) { - final String sql = "SELECT COUNT(application_table_name) FROM x_registered_table WHERE registered_table_name = ?"; - final int count = jdbcTemplate.queryForObject(sql, Integer.class, datatable); - if (count == 0) { - throw new DatatableNotFoundException(datatable); - } - } - - private String getFKField(final String applicationTableName) { - return applicationTableName.substring(2) + "_id"; + private String getFKField(EntityTables entityTable) { + return entityTable.getForeignKeyColumnNameOnDatatable(); } private String getAddSql(final List columnHeaders, final String datatable, final String fkName, @@ -1897,7 +1618,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ if (StringUtils.isEmpty(pValue) || "null".equalsIgnoreCase(pValue)) { pValueWrite = "null"; } else { - if ("bit".equalsIgnoreCase(pColumnHeader.getColumnType())) { + if (pColumnHeader.getColumnType() == BIT) { if (databaseTypeResolver.isMySQL()) { pValueWrite = BooleanUtils.toString(BooleanUtils.toBooleanObject(pValue), "1", "0", "null"); } else if (databaseTypeResolver.isPostgreSQL()) { @@ -2016,7 +1737,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ if (StringUtils.isEmpty(pValue) || "null".equalsIgnoreCase(pValue)) { pValueWrite = "null"; } else { - if ("bit".equalsIgnoreCase(pColumnHeader.getColumnType())) { + if (pColumnHeader.getColumnType() == BIT) { if (databaseTypeResolver.isMySQL()) { pValueWrite = BooleanUtils.toString(BooleanUtils.toBooleanObject(pValue), "1", "0", "null"); } else if (databaseTypeResolver.isPostgreSQL()) { @@ -2050,7 +1771,6 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ private Map getAffectedAndChangedColumns(final GenericResultsetData grs, final Map queryParams, final String fkName) { - final Map affectedColumns = getAffectedColumns(grs.getColumnHeaders(), queryParams, fkName); final Map affectedAndChangedColumns = new HashMap<>(); @@ -2065,15 +1785,12 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } private boolean columnChanged(final String key, final Object value, final GenericResultsetData grs) { - final List columnValues = grs.getData().get(0).getRow(); Object columnValue = null; for (int i = 0; i < grs.getColumnHeaders().size(); i++) { - if (key.equals(grs.getColumnHeaders().get(i).getColumnName())) { columnValue = columnValues.get(i); - return notTheSame(columnValue, value); } } @@ -2098,6 +1815,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ String queryParamColumnUnderscored; String columnHeaderUnderscored; boolean notFound; + DatabaseType dialect = databaseTypeResolver.databaseType(); final Map affectedColumns = new HashMap<>(); final Set keys = queryParams.keySet(); @@ -2105,8 +1823,9 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ // ignores id and foreign key fields // also ignores locale and dateformat fields that are used for // validating numeric and date data - if (!(key.equalsIgnoreCase("id") || key.equalsIgnoreCase(keyFieldName) || key.equals("locale") || key.equals("dateFormat") - || key.equals(DataTableApiConstant.CREATEDAT_FIELD_NAME) || key.equals(DataTableApiConstant.UPDATEDAT_FIELD_NAME))) { + if (!(key.equalsIgnoreCase(TABLE_FIELD_ID) || key.equalsIgnoreCase(keyFieldName) || key.equals("locale") + || key.equals("dateFormat") || key.equals(DataTableApiConstant.CREATEDAT_FIELD_NAME) + || key.equals(DataTableApiConstant.UPDATEDAT_FIELD_NAME))) { notFound = true; // matches incoming fields with and without underscores (spaces // and underscores considered the same) @@ -2116,7 +1835,8 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ columnHeaderUnderscored = this.genericDataService.replace(columnHeader.getColumnName(), space, underscore); if (queryParamColumnUnderscored.equalsIgnoreCase(columnHeaderUnderscored)) { pValue = queryParams.get(key); - validatedValue = validateColumn(columnHeader, pValue, dateFormat, clientApplicationLocale); + validatedValue = SearchUtil.parseAndValidateColumnValue(columnHeader, pValue, dateFormat, + clientApplicationLocale, dialect); affectedColumns.put(columnHeader.getColumnName(), validatedValue); notFound = false; } @@ -2131,117 +1851,18 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ return affectedColumns; } - private Object validateColumn(final ResultsetColumnHeaderData columnHeader, final String pValue, final String dateFormat, - final Locale clientApplicationLocale) { - - String paramValue = pValue; - if (columnHeader.isDateDisplayType() || columnHeader.isDateTimeDisplayType() || columnHeader.isIntegerDisplayType() - || columnHeader.isDecimalDisplayType() || columnHeader.isBooleanDisplayType()) { - // only trim if string is not empty and is not null. - // throws a NULL pointer exception if the check below is not applied - paramValue = StringUtils.isNotEmpty(paramValue) ? paramValue.trim() : paramValue; - } - - if (StringUtils.isEmpty(paramValue) && columnHeader.isMandatory()) { - - final List dataValidationErrors = new ArrayList<>(); - final ApiParameterError error = ApiParameterError.parameterError("error.msg.column.mandatory", "Mandatory", - columnHeader.getColumnName()); - dataValidationErrors.add(error); - throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", - dataValidationErrors); - } - - if (StringUtils.isNotEmpty(paramValue)) { - if (columnHeader.hasColumnValues()) { - if (columnHeader.isCodeValueDisplayType()) { - - if (columnHeader.isColumnValueNotAllowed(paramValue)) { - final List dataValidationErrors = new ArrayList<>(); - final ApiParameterError error = ApiParameterError.parameterError("error.msg.invalid.columnValue", - "Value not found in Allowed Value list", columnHeader.getColumnName(), paramValue); - dataValidationErrors.add(error); - throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", - dataValidationErrors); - } - - return paramValue; - } else if (columnHeader.isCodeLookupDisplayType()) { - - final Integer codeLookup = Integer.valueOf(paramValue); - if (columnHeader.isColumnCodeNotAllowed(codeLookup)) { - final List dataValidationErrors = new ArrayList<>(); - final ApiParameterError error = ApiParameterError.parameterError("error.msg.invalid.columnValue", - "Value not found in Allowed Value list", columnHeader.getColumnName(), paramValue); - dataValidationErrors.add(error); - throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", - dataValidationErrors); - } - - return codeLookup; - } else { - throw new PlatformDataIntegrityException("error.msg.invalid.columnType.", "Code: " + columnHeader.getColumnName() - + " - Invalid Type " + columnHeader.getColumnType() + " (neither varchar nor int)"); - } - } - - if (columnHeader.isDateDisplayType()) { - return JsonParserHelper.convertFrom(paramValue, columnHeader.getColumnName(), dateFormat, clientApplicationLocale); - } else if (columnHeader.isDateTimeDisplayType()) { - return JsonParserHelper.convertDateTimeFrom(paramValue, columnHeader.getColumnName(), dateFormat, clientApplicationLocale); - } else if (columnHeader.isIntegerDisplayType()) { - return this.helper.convertToInteger(paramValue, columnHeader.getColumnName(), clientApplicationLocale); - } else if (columnHeader.isDecimalDisplayType()) { - return this.helper.convertFrom(paramValue, columnHeader.getColumnName(), clientApplicationLocale); - } else if (columnHeader.isBooleanDisplayType()) { - - final Boolean tmpBoolean = BooleanUtils.toBooleanObject(paramValue); - if (tmpBoolean == null) { - final ApiParameterError error = ApiParameterError - .parameterError( - "validation.msg.invalid.boolean.format", "The parameter " + columnHeader.getColumnName() - + " has value: " + paramValue + " which is invalid boolean value.", - columnHeader.getColumnName(), paramValue); - final List dataValidationErrors = new ArrayList<>(); - dataValidationErrors.add(error); - throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", - dataValidationErrors); - } - return tmpBoolean; - } else if (columnHeader.isString()) { - if (columnHeader.getColumnLength() > 0 && paramValue.length() > columnHeader.getColumnLength()) { - final ApiParameterError error = ApiParameterError.parameterError( - "validation.msg.datatable.entry.column.exceeds.maxlength", - "The column `" + columnHeader.getColumnName() + "` exceeds its defined max-length ", - columnHeader.getColumnName(), paramValue); - final List dataValidationErrors = new ArrayList<>(); - dataValidationErrors.add(error); - throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", - dataValidationErrors); - } - } - } else { - paramValue = null; - } - return paramValue; - } - private String getDeleteEntriesSql(final String datatable, final String FKField, final Long appTableId) { - return "delete from " + sqlGenerator.escape(datatable) + " where " + sqlGenerator.escape(FKField) + " = " + appTableId; - } private String getDeleteEntrySql(final String datatable, final Long datatableId) { - - return "delete from " + sqlGenerator.escape(datatable) + " where id = " + datatableId; - + return "delete from " + sqlGenerator.escape(datatable) + " where " + TABLE_FIELD_ID + " = " + datatableId; } private boolean isMultirowDatatable(final List columnHeaders) { boolean multiRow = false; for (ResultsetColumnHeaderData column : columnHeaders) { - if (column.isNamed("id")) { + if (column.isNamed(TABLE_FIELD_ID)) { multiRow = true; break; } @@ -2257,10 +1878,9 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } // Equals would fail if the scale is not the same if (currValue instanceof BigDecimal && pValue instanceof BigDecimal) { - return !(((BigDecimal) currValue).compareTo((BigDecimal) pValue) == 0); - } else { - return !currValue.equals(pValue); + return !MathUtil.isEqualTo((BigDecimal) currValue, (BigDecimal) pValue); } + return !currValue.equals(pValue); } @Override @@ -2274,11 +1894,71 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ public boolean isDatatableAttachedToEntityDatatableCheck(final String datatableName) { StringBuilder builder = new StringBuilder(); - builder.append(" SELECT COUNT(edc.x_registered_table_name) FROM x_registered_table xrt "); + builder.append(" SELECT COUNT(edc.x_registered_table_name) FROM x_registered_table xrt"); builder.append(" JOIN m_entity_datatable_check edc ON edc.x_registered_table_name = xrt.registered_table_name"); builder.append(" WHERE edc.x_registered_table_name = '" + datatableName + "'"); final Long count = this.jdbcTemplate.queryForObject(builder.toString(), Long.class); return count > 0; } + // --- DbUtils --- + + @NotNull + private String mapApiTypeToDbType(@NotNull String apiType, Integer length) { + if (StringUtils.isEmpty(apiType)) { + return ""; + } + JdbcJavaType jdbcType = DatatableCommandFromApiJsonDeserializer.mapApiTypeToJdbcType(apiType); + DatabaseType dialect = databaseTypeResolver.databaseType(); + if (jdbcType.isDecimalType()) { + return jdbcType.formatSql(dialect, 19, 6); // TODO: parameter length is not used + } else if (apiType.equalsIgnoreCase(API_FIELD_TYPE_DROPDOWN)) { + return jdbcType.formatSql(dialect, 11); // TODO: parameter length is not used + } + return jdbcType.formatSql(dialect, length); + } + + // --- Validation --- + + private EntityTables resolveEntity(final String entityName) { + EntityTables entityTable = EntityTables.fromEntityName(entityName); + if (entityTable == null) { + throw new PlatformDataIntegrityException("error.msg.invalid.application.table", "Invalid Datatable entity: " + entityName, + API_FIELD_NAME, entityName); + } + return entityTable; + } + + private void validateDatatableName(final String name) { + if (name == null || name.isEmpty()) { + throw new PlatformDataIntegrityException("error.msg.datatables.datatable.null.name", "Data table name must not be blank."); + } else if (!name.matches(DATATABLE_NAME_REGEX_PATTERN)) { + throw new PlatformDataIntegrityException("error.msg.datatables.datatable.invalid.name.regex", "Invalid data table name.", name); + } + SQLInjectionValidator.validateSQLInput(name); + } + + private String validateDatatableRegistered(String datatable) { + validateDatatableName(datatable); + if (!isRegisteredDataTable(datatable)) { + throw new DatatableNotFoundException(datatable); + } + return datatable; + } + + private boolean isRegisteredDataTable(final String datatable) { + final String sql = "SELECT COUNT(application_table_name) FROM " + TABLE_REGISTERED_TABLE + " WHERE registered_table_name = ?"; + final int count = jdbcTemplate.queryForObject(sql, Integer.class, datatable); + return count > 0; + } + + private void validateDataTableExists(final String datatableName) { + final String sql = "select (CASE WHEN exists (select 1 from information_schema.tables where table_schema = " + + sqlGenerator.currentSchema() + " and table_name = ?) THEN 'true' ELSE 'false' END)"; + final boolean dataTableExists = Boolean.parseBoolean(this.jdbcTemplate.queryForObject(sql, String.class, datatableName)); + if (!dataTableExists) { + throw new PlatformDataIntegrityException("error.msg.invalid.datatable", "Invalid Data Table: " + datatableName, API_FIELD_NAME, + datatableName); + } + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropTransactionData.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropTransactionData.java index 485850f24..dfdd0c5e2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropTransactionData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropTransactionData.java @@ -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; diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/MoneyData.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/MoneyData.java index 181a3a573..6262097ed 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/MoneyData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/MoneyData.java @@ -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 { diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java index ce0473ed6..f3b679610 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java @@ -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; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index 807342ad9..5816beea1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -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; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index 039da3468..bb11ce273 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -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; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java index 2a757b5e1..9740bbec7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java @@ -1230,7 +1230,7 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead } final List 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,"); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java index cd8967f91..c9910a9b9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java @@ -207,8 +207,8 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi final Map 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); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java index a55e6635c..eea41b942 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java @@ -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 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 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 changes = savingsAccount.applicantWithdrawsFromApplication(currentUser, command, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/SearchConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/SearchConstants.java index 975963db7..48a327c58 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/SearchConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/SearchConstants.java @@ -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 { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/service/SearchUtil.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/service/SearchUtil.java new file mode 100644 index 000000000..87a5653fb --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/service/SearchUtil.java @@ -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 mapHeadersToName(@NotNull Collection columnHeaders) { + return columnHeaders.stream().collect(Collectors.toMap(ResultsetColumnHeaderData::getColumnName, e -> e)); + } + + public static ResultsetColumnHeaderData findFiltered(@NotNull Collection columnHeaders, + @NotNull Predicate filter) { + return columnHeaders.stream().filter(filter).findFirst().orElse(null); + } + + public static ResultsetColumnHeaderData getFiltered(@NotNull Collection columnHeaders, + @NotNull Predicate 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 selectColumns, + @NotNull List resultColumns, @NotNull List results) { + JsonObject json = new JsonObject(); + for (int i = 0; i < selectColumns.size(); i++) { + String sCol = selectColumns.get(i).replaceAll("\"", ""); + Object rowValue = rowSet.getObject(sCol); + 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 validateToJdbcColumns(List columns, Map columnHeaders, + List errors, boolean allowEmpty) { + List 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 columnHeaders, + List 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 columnHeaders, DatabaseType dialect) { + List 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(); + } +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImplTest.java index b3216baf7..caa5a9f9f 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImplTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImplTest.java @@ -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 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, diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/interoperation/InteropTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/interoperation/InteropTest.java index 409a9d397..0cce0b4b4 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/interoperation/InteropTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/interoperation/InteropTest.java @@ -34,6 +34,7 @@ import java.math.RoundingMode; import java.util.HashMap; import java.util.Map; import java.util.UUID; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.Utils; import org.apache.fineract.integrationtests.common.accounting.Account; @@ -46,7 +47,6 @@ import org.apache.fineract.interoperation.domain.InteropActionState; import org.apache.fineract.interoperation.domain.InteropIdentifierType; import org.apache.fineract.interoperation.domain.InteropTransactionRole; import org.apache.fineract.interoperation.util.InteropUtil; -import org.apache.fineract.interoperation.util.MathUtil; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; import org.apache.fineract.portfolio.savings.SavingsApiConstants; import org.junit.jupiter.api.Assertions;