diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/PagedLocalRequest.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/PagedLocalRequest.java new file mode 100644 index 000000000..b2c645442 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/PagedLocalRequest.java @@ -0,0 +1,41 @@ +/** + * 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; + +import java.util.Locale; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper; + +@Getter +@Setter +@RequiredArgsConstructor +public class PagedLocalRequest extends PagedRequest { + + private String dateFormat; + + private String dateTimeFormat; + + private String locale; + + public Locale getLocaleObject() { + return locale == null ? null : JsonParserHelper.localeFromString(locale); + } +} 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 index ebfbade9d..a03834de0 100644 --- 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 @@ -30,14 +30,14 @@ public enum JdbcJavaType { @Override public Object toJdbcValueImpl(@NotNull DatabaseType dialect, Object value) { - return Boolean.TRUE.equals(value) ? 1 : 0; + return value == null ? null : (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); + return (value != null && 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")), // 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 index 9c4d65d20..3a2e101c8 100644 --- 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 @@ -47,7 +47,7 @@ public enum SqlOperator { } @Override - public String formatPlaceholderImpl(String definition, String placeholder) { + public String formatPlaceholderImpl(String definition, int paramCount, String placeholder) { return format("%s %s CONCAT('%%', %s, '%%')", definition, getSymbol(), placeholder); } }, @@ -60,7 +60,7 @@ public enum SqlOperator { } @Override - public String formatPlaceholderImpl(String definition, String placeholder) { + public String formatPlaceholderImpl(String definition, int paramCount, String placeholder) { return format("%s %s CONCAT('%%', %s, '%%')", definition, getSymbol(), placeholder); } }, @@ -74,7 +74,7 @@ public enum SqlOperator { } @Override - public String formatPlaceholderImpl(String definition, String placeholder) { + public String formatPlaceholderImpl(String definition, int paramCount, String placeholder) { return format("%s %s %s AND %s", definition, getSymbol(), placeholder, placeholder); } }, @@ -88,7 +88,7 @@ public enum SqlOperator { } @Override - public String formatPlaceholderImpl(String definition, String placeholder) { + public String formatPlaceholderImpl(String definition, int paramCount, String placeholder) { return format("%s %s %s AND %s", definition, getSymbol(), placeholder, placeholder); } }, @@ -102,8 +102,13 @@ public enum SqlOperator { } @Override - public boolean isPlaceholderSupported() { - return false; + protected String formatPlaceholderImpl(String definition, int paramCount, String placeholder) { + return format("%s %s (%s)", definition, getSymbol(), placeholder + (", " + placeholder).repeat(paramCount - 1)); + } + + @Override + protected String formatNamedParamImpl(String definition, int paramCount, String namedParam) { + return format("%s %s (%s)", definition, getSymbol(), namedParam); } }, NIN("NOT IN", -1) { // @@ -116,8 +121,13 @@ public enum SqlOperator { } @Override - public boolean isPlaceholderSupported() { - return false; + protected String formatPlaceholderImpl(String definition, int paramCount, String placeholder) { + return format("%s %s (%s)", definition, getSymbol(), placeholder + (", " + placeholder).repeat(paramCount - 1)); + } + + @Override + protected String formatNamedParamImpl(String definition, int paramCount, String namedParam) { + return format("%s %s (%s)", definition, getSymbol(), namedParam); } }, NULL("IS NULL", 0), // @@ -160,38 +170,45 @@ public enum SqlOperator { : 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"); + public String formatNamedParam(@NotNull DatabaseSpecificSQLGenerator sqlGenerator, String definition, int paramCount, String alias) { + validateParamCount(paramCount); + if (paramCount > 1) { + throw new PlatformServiceUnavailableException("error.msg.database.operator.named.invalid", + "Named parameter is not allowed on " + this); } + return formatNamedParamImpl(sqlGenerator.alias(sqlGenerator.escape(definition), alias), paramCount, ":" + definition); + } + + protected String formatNamedParamImpl(String definition, int paramCount, String namedParam) { + return formatPlaceholderImpl(definition, paramCount, namedParam); + } + + public String formatPlaceholder(@NotNull DatabaseSpecificSQLGenerator sqlGenerator, String definition, int paramCount, String alias) { + return formatPlaceholder(sqlGenerator, definition, paramCount, alias, "?"); + } + + public String formatPlaceholder(@NotNull DatabaseSpecificSQLGenerator sqlGenerator, String definition, int paramCount, String alias, + String placeholder) { + validateParamCount(paramCount); + return formatPlaceholderImpl(sqlGenerator.alias(sqlGenerator.escape(definition), alias), paramCount, placeholder); + } + + protected String formatPlaceholderImpl(String definition, int paramCount, String placeholder) { 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); - } + validateParamCount(values == null ? 0 : values.length); } public void validateValues(List values) { - int size = values == null ? 0 : values.size(); - if (paramCount < 0 ? size < -paramCount : size != paramCount) { + validateParamCount(values == null ? 0 : values.size()); + } + + public void validateParamCount(int paramCount) { + if (this.paramCount < 0 ? paramCount < -this.paramCount : paramCount != this.paramCount) { throw new PlatformServiceUnavailableException("error.msg.database.operator.invalid", - "Number of parameters " + size + " must be " + Math.abs(paramCount) + " on " + this); + "Number of parameters " + paramCount + " must be " + Math.abs(this.paramCount) + " on " + this); } } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/data/DatatableSearchRequest.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/data/DatatableSearchRequest.java new file mode 100644 index 000000000..87b4a9121 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/data/DatatableSearchRequest.java @@ -0,0 +1,39 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.dataqueries.data; + +import java.io.Serializable; +import java.util.List; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Immutable data object representing datatable data. + */ +@Data +@NoArgsConstructor +public final class DatatableSearchRequest implements Serializable { + + private List columnFilters; + + private List resultColumns; + + private String datatable; + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DatatablesApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DatatablesApiResource.java index 843753010..f7c0919ff 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DatatablesApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DatatablesApiResource.java @@ -52,11 +52,14 @@ import org.apache.fineract.infrastructure.core.api.ApiParameterHelper; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer; +import org.apache.fineract.infrastructure.core.service.PagedLocalRequest; import org.apache.fineract.infrastructure.dataqueries.data.DatatableData; import org.apache.fineract.infrastructure.dataqueries.data.GenericResultsetData; import org.apache.fineract.infrastructure.dataqueries.service.GenericDataService; import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.search.data.AdvancedQueryData; +import org.springframework.data.domain.Page; import org.springframework.stereotype.Component; @Path("/v1/datatables") @@ -225,6 +228,19 @@ public class DatatablesApiResource { return this.toApiJsonSerializer.serializePretty(ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters()), result); } + @POST + @Path("{datatable}/query") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Query Data Table values", description = "Query values from a registered data table.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = List.class))) }) + public String advancedQuery(@PathParam("datatable") @Parameter(description = "datatable") final String datatable, + PagedLocalRequest queryRequest, @Context final UriInfo uriInfo) { + final Page result = this.readWriteNonCoreDataService.queryDataTableAdvanced(datatable, queryRequest); + return this.toApiJsonSerializer.serializePretty(ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters()), result); + } + @GET @Path("{datatable}/{apptableId}") @Consumes({ MediaType.APPLICATION_JSON }) diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/data/DataTableValidator.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/data/DataTableValidator.java index c6e9b8213..0a5a552cc 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/data/DataTableValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/data/DataTableValidator.java @@ -18,8 +18,16 @@ */ package org.apache.fineract.infrastructure.dataqueries.data; +import static org.apache.fineract.portfolio.search.SearchConstants.API_PARAM_COLUMN; +import static org.apache.fineract.portfolio.search.SearchConstants.API_PARAM_FILTERS; +import static org.apache.fineract.portfolio.search.SearchConstants.API_PARAM_OPERATOR; +import static org.apache.fineract.portfolio.search.SearchConstants.API_PARAM_QUERY; +import static org.apache.fineract.portfolio.search.SearchConstants.API_PARAM_RESULTCOLUMNS; +import static org.apache.fineract.portfolio.search.SearchConstants.API_PARAM_TABLE; + 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; @@ -29,9 +37,13 @@ import java.util.Map; import java.util.Set; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; -import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant; +import org.apache.fineract.portfolio.search.data.AdvancedQueryData; +import org.apache.fineract.portfolio.search.data.AdvancedQueryRequest; +import org.apache.fineract.portfolio.search.data.ColumnFilterData; +import org.apache.fineract.portfolio.search.data.FilterData; +import org.apache.fineract.portfolio.search.data.TableQueryData; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -64,9 +76,56 @@ public class DataTableValidator { baseDataValidator.reset().parameter(DataTableApiConstant.categoryParamName).value(category).isOneOfTheseValues(objectArray); } - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } + baseDataValidator.throwValidationErrors(); + } + public void validateTableSearch(@NotNull AdvancedQueryRequest queryRequest) { + final List errors = new ArrayList<>(); + final DataValidatorBuilder validator = new DataValidatorBuilder(errors).resource(DataTableApiConstant.DATATABLE_RESOURCE_NAME); + AdvancedQueryData baseQuery = queryRequest.getBaseQuery(); + if (baseQuery != null) { + validateQueryData(baseQuery, validator); + } + List datatableQueries = queryRequest.getDatatableQueries(); + if (datatableQueries != null) { + for (TableQueryData datatableQuery : datatableQueries) { + validator.reset().parameter(API_PARAM_TABLE).value(datatableQuery.getTable()).notBlank(); + AdvancedQueryData queryData = datatableQuery.getQuery(); + validator.reset().parameter(API_PARAM_QUERY).value(queryData).notBlank(); + if (queryData != null) { + validateQueryData(queryData, validator); + } + } + } + validator.throwValidationErrors(); + } + + public void validateTableSearch(@NotNull AdvancedQueryData queryData) { + final DataValidatorBuilder validator = new DataValidatorBuilder(new ArrayList<>()) + .resource(DataTableApiConstant.DATATABLE_RESOURCE_NAME); + validateQueryData(queryData, validator); + validator.throwValidationErrors(); + } + + private void validateQueryData(@NotNull AdvancedQueryData queryData, @NotNull DataValidatorBuilder validator) { + List columnFilters = queryData.getColumnFilters(); + if (columnFilters != null) { + for (ColumnFilterData columnFilter : columnFilters) { + validator.reset().parameter(API_PARAM_COLUMN).value(columnFilter.getColumn()).notNull(); + List filters = columnFilter.getFilters(); + validator.reset().parameter(API_PARAM_FILTERS).value(filters == null ? null : filters.toArray()).notNull().arrayNotEmpty(); + if (filters != null) { + for (FilterData filter : filters) { + validator.reset().parameter(API_PARAM_OPERATOR).value(filter.getOperator()).notNull(); + } + } + } + List resultColumns = queryData.getResultColumns(); + if (resultColumns != null) { + for (String resultColumn : resultColumns) { + validator.reset().parameter(API_PARAM_RESULTCOLUMNS).value(resultColumn).notBlank(); + } + } + } } } 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 fde2316ab..2db319073 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 @@ -56,15 +56,20 @@ public enum EntityTables { private static final Map BY_ENTITY_NAME = Arrays.stream(VALUES) .collect(Collectors.toMap(EntityTables::getName, e -> e)); + @NotNull private final String name; + @NotNull private final String apptableName; + @NotNull private final String foreignKeyColumnNameOnDatatable; + @NotNull private final String refColumn; // referenced column name on apptable private final ImmutableList checkStatuses; - EntityTables(String name, String apptableName, String foreignKeyColumnNameOnDatatable, String refColumn, StatusEnum... statuses) { + EntityTables(@NotNull String name, @NotNull String apptableName, @NotNull String foreignKeyColumnNameOnDatatable, + @NotNull String refColumn, StatusEnum... statuses) { this.name = name; this.apptableName = apptableName; this.foreignKeyColumnNameOnDatatable = foreignKeyColumnNameOnDatatable; @@ -72,22 +77,26 @@ public enum EntityTables { this.checkStatuses = statuses == null ? ImmutableList.of() : ImmutableList.copyOf(statuses); } - EntityTables(String name, String foreignKeyColumnNameOnDatatable, String refColumn, StatusEnum... statuses) { + EntityTables(@NotNull String name, @NotNull String foreignKeyColumnNameOnDatatable, @NotNull String refColumn, StatusEnum... statuses) { this(name, name, foreignKeyColumnNameOnDatatable, refColumn, statuses); } + @NotNull public String getName() { return name; } + @NotNull public String getApptableName() { return apptableName; } + @NotNull public String getForeignKeyColumnNameOnDatatable() { return this.foreignKeyColumnNameOnDatatable; } + @NotNull public String getRefColumn() { return refColumn; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java index e10aa9a06..cef0a1f4f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java @@ -21,10 +21,15 @@ package org.apache.fineract.infrastructure.dataqueries.service; import com.google.gson.JsonObject; import jakarta.validation.constraints.NotNull; import java.util.List; +import java.util.Locale; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.service.PagedLocalRequest; import org.apache.fineract.infrastructure.dataqueries.data.DatatableData; +import org.apache.fineract.infrastructure.dataqueries.data.EntityTables; import org.apache.fineract.infrastructure.dataqueries.data.GenericResultsetData; +import org.apache.fineract.portfolio.search.data.AdvancedQueryData; +import org.springframework.data.domain.Page; import org.springframework.security.access.prepost.PreAuthorize; public interface ReadWriteNonCoreDataService { @@ -75,4 +80,10 @@ public interface ReadWriteNonCoreDataService { List queryDataTable(@NotNull String datatable, @NotNull String columnName, String columnValue, @NotNull String resultColumns); + + Page queryDataTableAdvanced(@NotNull String datatable, @NotNull PagedLocalRequest pagedRequest); + + boolean buildDataQueryEmbedded(@NotNull EntityTables entityTable, @NotNull String datatable, @NotNull AdvancedQueryData request, + @NotNull List selectColumns, @NotNull StringBuilder select, @NotNull StringBuilder from, @NotNull StringBuilder where, + @NotNull List params, String mainAlias, String alias, String dateFormat, String dateTimeFormat, Locale locale); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java index bf2a82db6..fb8184c32 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 @@ -83,6 +83,7 @@ import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavaila import org.apache.fineract.infrastructure.core.serialization.DatatableCommandFromApiJsonDeserializer; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.infrastructure.core.service.PagedLocalRequest; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; import org.apache.fineract.infrastructure.core.service.database.DatabaseType; import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver; @@ -101,11 +102,17 @@ 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.data.AdvancedQueryData; +import org.apache.fineract.portfolio.search.data.ColumnFilterData; import org.apache.fineract.portfolio.search.service.SearchUtil; import org.apache.fineract.useradministration.domain.AppUser; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.support.PageableExecutionUtils; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.support.GeneratedKeyHolder; @@ -199,20 +206,16 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ public List queryDataTable(@NotNull String datatable, @NotNull String columnName, String columnValueString, @NotNull String resultColumnsString) { datatable = validateDatatableRegistered(datatable); - Map columnHeaders = SearchUtil + Map headersByName = SearchUtil .mapHeadersToName(genericDataService.fillResultsetColumnHeaders(datatable)); - 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); - } + List selectColumns = SearchUtil.validateToJdbcColumnNames(resultColumns, headersByName, false); + ResultsetColumnHeaderData column = SearchUtil.validateToJdbcColumn(columnName, headersByName, false); - Object columnValue = SearchUtil.parseAndValidateJdbcColumnValue(columnName, columnValueString, columnHeaders, false, sqlGenerator); + Object columnValue = SearchUtil.parseJdbcColumnValue(column, columnValueString, null, null, null, false, sqlGenerator); String sql = sqlGenerator.buildSelect(selectColumns, null, false) + " " + sqlGenerator.buildFrom(datatable, null, false) + " WHERE " - + EQ.formatPlaceholder(sqlGenerator, columnName, null); + + EQ.formatPlaceholder(sqlGenerator, column.getColumnName(), 1, null); SqlRowSet rowSet = jdbcTemplate.queryForRowSet(sql, columnValue); List results = new ArrayList<>(); @@ -222,6 +225,110 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ return results; } + @Override + public Page queryDataTableAdvanced(@NotNull String datatable, @NotNull PagedLocalRequest pagedRequest) { + datatable = validateDatatableRegistered(datatable); + context.authenticatedUser().validateHasDatatableReadPermission(datatable); + + AdvancedQueryData request = pagedRequest.getRequest().orElseThrow(); + dataTableValidator.validateTableSearch(request); + + Map headersByName = SearchUtil + .mapHeadersToName(genericDataService.fillResultsetColumnHeaders(datatable)); + String pkColumn = SearchUtil.getFiltered(headersByName.values(), ResultsetColumnHeaderData::getIsColumnPrimaryKey).getColumnName(); + + List columnFilters = request.getNonNullFilters(); + columnFilters.forEach(e -> e.setColumn(SearchUtil.validateToJdbcColumnName(e.getColumn(), headersByName, false))); + + List resultColumns = request.getNonNullResultColumns(); + List selectColumns; + if (resultColumns.isEmpty()) { + resultColumns.add(pkColumn); + selectColumns = new ArrayList<>(); + selectColumns.add(pkColumn); + } else { + selectColumns = SearchUtil.validateToJdbcColumnNames(resultColumns, headersByName, false); + } + PageRequest pageable = pagedRequest.toPageable(); + PageRequest sortPageable; + if (pageable.getSort().isSorted()) { + List orders = pageable.getSort().toList(); + sortPageable = pageable.withSort(Sort.by(orders.stream() + .map(e -> e.withProperty(SearchUtil.validateToJdbcColumnName(e.getProperty(), headersByName, false))).toList())); + } else { + pageable = pageable.withSort(Sort.Direction.DESC, pkColumn); + sortPageable = pageable; + } + + String dateFormat = pagedRequest.getDateFormat(); + String dateTimeFormat = pagedRequest.getDateTimeFormat(); + Locale locale = pagedRequest.getLocaleObject(); + + String select = sqlGenerator.buildSelect(selectColumns, null, false); + String from = " " + sqlGenerator.buildFrom(datatable, null, false); + StringBuilder where = new StringBuilder(); + ArrayList params = new ArrayList<>(); + SearchUtil.buildQueryCondition(columnFilters, where, params, null, headersByName, dateFormat, dateTimeFormat, locale, false, + sqlGenerator); + + List results = new ArrayList<>(); + Object[] args = params.toArray(); + + // Execute the count Query + String countQuery = "SELECT COUNT(*)" + from + where; + Integer totalElements = jdbcTemplate.queryForObject(countQuery, Integer.class, args); + if (totalElements == null || totalElements == 0) { + return PageableExecutionUtils.getPage(results, pageable, () -> 0); + } + + StringBuilder query = new StringBuilder().append(select).append(from).append(where); + query.append(" ").append(sqlGenerator.buildOrderBy(sortPageable.getSort().toList(), null, false)); + if (pageable.isPaged()) { + query.append(" ").append(sqlGenerator.limit(pageable.getPageSize(), (int) pageable.getOffset())); + } + + SqlRowSet rowSet = jdbcTemplate.queryForRowSet(query.toString(), args); + + while (rowSet.next()) { + SearchUtil.extractJsonResult(rowSet, selectColumns, resultColumns, results); + } + return PageableExecutionUtils.getPage(results, pageable, () -> totalElements); + } + + @Override + public boolean buildDataQueryEmbedded(@NotNull EntityTables entityTable, @NotNull String datatable, @NotNull AdvancedQueryData request, + @NotNull List selectColumns, @NotNull StringBuilder select, @NotNull StringBuilder from, @NotNull StringBuilder where, + @NotNull List params, String mainAlias, String alias, String dateFormat, String dateTimeFormat, Locale locale) { + List resultColumns = request.getResultColumns(); + List columnFilters = request.getColumnFilters(); + if ((resultColumns == null || resultColumns.isEmpty()) && (columnFilters == null || columnFilters.isEmpty())) { + return false; + } + + datatable = validateDatatableRegistered(datatable); + context.authenticatedUser().validateHasDatatableReadPermission(datatable); + + Map headersByName = SearchUtil + .mapHeadersToName(genericDataService.fillResultsetColumnHeaders(datatable)); + + List thisSelectColumns = SearchUtil.validateToJdbcColumnNames(resultColumns, headersByName, true); + if (columnFilters != null) { + columnFilters.forEach(e -> e.setColumn(SearchUtil.validateToJdbcColumnName(e.getColumn(), headersByName, false))); + } + + select.append(sqlGenerator.buildSelect(thisSelectColumns, alias, true)); + selectColumns.addAll(thisSelectColumns); + + String joinType = "LEFT"; + if (SearchUtil.buildQueryCondition(columnFilters, where, params, alias, headersByName, dateFormat, dateTimeFormat, locale, true, + sqlGenerator)) { + joinType = null; // INNER + } + from.append(sqlGenerator.buildJoin(datatable, alias, entityTable.getForeignKeyColumnNameOnDatatable(), mainAlias, + entityTable.getRefColumn(), joinType)); + return true; + } + private void logAsErrorUnexpectedDataIntegrityException(final Exception dve) { log.error("Error occurred.", dve); } @@ -1830,7 +1937,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ columnHeaderUnderscored = this.genericDataService.replace(columnHeader.getColumnName(), space, underscore); if (queryParamColumnUnderscored.equalsIgnoreCase(columnHeaderUnderscored)) { pValue = queryParams.get(key); - validatedValue = SearchUtil.parseAndValidateColumnValue(columnHeader, pValue, dateFormat, + validatedValue = SearchUtil.parseColumnValue(columnHeader, pValue, dateFormat, dateFormat, clientApplicationLocale, true, sqlGenerator); affectedColumns.put(columnHeader.getColumnName(), validatedValue); notFound = false; @@ -1890,7 +1997,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ + " JOIN m_entity_datatable_check edc ON edc.x_registered_table_name = xrt.registered_table_name" + " WHERE edc.x_registered_table_name = '" + datatableName + "'"; final Long count = this.jdbcTemplate.queryForObject(sql, Long.class); - return count > 0; + return count != null && count > 0; } // --- DbUtils --- @@ -1940,8 +2047,8 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ 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; + final Integer count = jdbcTemplate.queryForObject(sql, Integer.class, datatable); + return count != null && count > 0; } private void validateDataTableExists(final String datatableName) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java index a1ed7911d..42e737eba 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.portfolio.savings.api; +import com.google.gson.JsonObject; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; @@ -39,12 +40,14 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.UriInfo; import java.math.BigDecimal; import java.util.Collection; +import java.util.List; import java.util.Locale; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.commands.service.CommandWrapperBuilder; import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; +import org.apache.fineract.infrastructure.core.api.ApiParameterHelper; import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; @@ -52,6 +55,7 @@ import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamE import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper; +import org.apache.fineract.infrastructure.core.service.PagedLocalRequest; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadPlatformService; @@ -60,6 +64,7 @@ import org.apache.fineract.portfolio.savings.SavingsApiConstants; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService; import org.apache.fineract.portfolio.savings.service.search.SavingsAccountTransactionSearchService; +import org.apache.fineract.portfolio.search.data.AdvancedQueryRequest; import org.apache.fineract.portfolio.search.data.TransactionSearchRequest; import org.springframework.dao.CannotAcquireLockException; import org.springframework.data.domain.Page; @@ -159,6 +164,19 @@ public class SavingsAccountTransactionsApiResource { return toApiJsonSerializer.serialize(transactionsData); } + @POST + @Path("query") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Advanced search Savings Account Transactions") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = List.class))) }) + public String advancedQuery(@PathParam("savingsId") @Parameter(description = "savingsId") final Long savingsId, + PagedLocalRequest queryRequest, @Context final UriInfo uriInfo) { + final Page result = transactionsSearchService.queryAdvanced(savingsId, queryRequest); + return this.toApiJsonSerializer.serializePretty(ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters()), result); + } + @POST @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionSearchService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionSearchService.java index 3bf5765cf..73e35bd55 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionSearchService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionSearchService.java @@ -18,12 +18,17 @@ */ package org.apache.fineract.portfolio.savings.service.search; +import com.google.gson.JsonObject; import jakarta.validation.constraints.NotNull; +import org.apache.fineract.infrastructure.core.service.PagedLocalRequest; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; +import org.apache.fineract.portfolio.search.data.AdvancedQueryRequest; import org.apache.fineract.portfolio.search.data.TransactionSearchRequest; import org.springframework.data.domain.Page; public interface SavingsAccountTransactionSearchService { Page searchTransactions(@NotNull Long savingsId, @NotNull TransactionSearchRequest searchParameters); + + Page queryAdvanced(@NotNull Long savingsId, @NotNull PagedLocalRequest pagedRequest); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionsSearchServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionsSearchServiceImpl.java index ba06548d8..db724134d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionsSearchServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/search/SavingsAccountTransactionsSearchServiceImpl.java @@ -20,27 +20,33 @@ package org.apache.fineract.portfolio.savings.service.search; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME; +import com.google.gson.JsonObject; import jakarta.validation.constraints.NotNull; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.function.Predicate; import lombok.RequiredArgsConstructor; -import org.apache.fineract.infrastructure.core.data.ApiParameterError; -import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.infrastructure.core.service.PagedLocalRequest; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; import org.apache.fineract.infrastructure.core.service.database.SqlOperator; +import org.apache.fineract.infrastructure.dataqueries.data.DataTableValidator; import org.apache.fineract.infrastructure.dataqueries.data.EntityTables; import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData; import org.apache.fineract.infrastructure.dataqueries.service.GenericDataService; +import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataServiceImpl; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformServiceImpl; +import org.apache.fineract.portfolio.search.data.AdvancedQueryData; +import org.apache.fineract.portfolio.search.data.AdvancedQueryRequest; import org.apache.fineract.portfolio.search.data.ColumnFilterData; +import org.apache.fineract.portfolio.search.data.TableQueryData; import org.apache.fineract.portfolio.search.data.TransactionSearchRequest; import org.apache.fineract.portfolio.search.service.SearchUtil; import org.jetbrains.annotations.Nullable; @@ -49,6 +55,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.support.PageableExecutionUtils; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.rowset.SqlRowSet; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -60,6 +67,8 @@ public class SavingsAccountTransactionsSearchServiceImpl implements SavingsAccou private final PlatformSecurityContext context; private final GenericDataService genericDataService; private final DatabaseSpecificSQLGenerator sqlGenerator; + private final ReadWriteNonCoreDataServiceImpl datatableService; + private final DataTableValidator dataTableValidator; private final JdbcTemplate jdbcTemplate; @Override @@ -68,19 +77,15 @@ public class SavingsAccountTransactionsSearchServiceImpl implements SavingsAccou context.authenticatedUser().validateHasReadPermission(SAVINGS_ACCOUNT_RESOURCE_NAME); String apptable = EntityTables.SAVINGS_TRANSACTION.getApptableName(); - Map columnHeaders = SearchUtil + Map headersByName = SearchUtil .mapHeadersToName(genericDataService.fillResultsetColumnHeaders(apptable)); PageRequest pageable = searchParameters.getPageable(); PageRequest sortPageable; if (pageable.getSort().isSorted()) { - List errors = new ArrayList<>(); List orders = pageable.getSort().toList(); sortPageable = pageable.withSort(Sort.by(orders.stream() - .map(e -> e.withProperty(SearchUtil.validateToJdbcColumn(e.getProperty(), columnHeaders, errors, false))).toList())); - if (!errors.isEmpty()) { - throw new PlatformApiDataValidationException(errors); - } + .map(e -> e.withProperty(SearchUtil.validateToJdbcColumnName(e.getProperty(), headersByName, false))).toList())); } else { pageable = pageable.withSort(Sort.Direction.DESC, "transaction_date", "created_date", "id"); sortPageable = pageable; @@ -102,9 +107,9 @@ public class SavingsAccountTransactionsSearchServiceImpl implements SavingsAccou } String alias = "tr"; - StringBuilder where = new StringBuilder(" WHERE "); + StringBuilder where = new StringBuilder(); ArrayList params = new ArrayList<>(); - SearchUtil.buildQueryCondition(columnFilters, where, params, alias, columnHeaders, false, false, sqlGenerator); + SearchUtil.buildQueryCondition(columnFilters, where, params, alias, headersByName, null, null, null, false, sqlGenerator); SavingsAccountReadPlatformServiceImpl.SavingsAccountTransactionsMapper tm = new SavingsAccountReadPlatformServiceImpl.SavingsAccountTransactionsMapper(); Object[] args = params.toArray(); @@ -171,4 +176,113 @@ public class SavingsAccountTransactionsSearchServiceImpl implements SavingsAccou } return false; } + + @Override + public Page queryAdvanced(@NotNull Long savingsId, @NotNull PagedLocalRequest pagedRequest) { + context.authenticatedUser().validateHasReadPermission(SAVINGS_ACCOUNT_RESOURCE_NAME); + String apptable = EntityTables.SAVINGS_TRANSACTION.getApptableName(); + + AdvancedQueryRequest queryRequest = pagedRequest.getRequest().orElseThrow(); + dataTableValidator.validateTableSearch(queryRequest); + + List columnHeaders = genericDataService.fillResultsetColumnHeaders(apptable); + Map headersByName = SearchUtil.mapHeadersToName(columnHeaders); + String pkColumn = SearchUtil.getFiltered(columnHeaders, ResultsetColumnHeaderData::getIsColumnPrimaryKey).getColumnName(); + + AdvancedQueryData baseQuery = queryRequest.getBaseQuery(); + List datatableQueries = queryRequest.getDatatableQueries(); + + List columnFilters; + List resultColumns; + List selectColumns; + if (baseQuery == null) { + columnFilters = new ArrayList<>(); + resultColumns = new ArrayList<>(); + selectColumns = new ArrayList<>(); + } else { + columnFilters = baseQuery.getNonNullFilters(); + columnFilters.forEach(e -> e.setColumn(SearchUtil.validateToJdbcColumnName(e.getColumn(), headersByName, false))); + resultColumns = baseQuery.getNonNullResultColumns(); + selectColumns = new ArrayList<>(SearchUtil.validateToJdbcColumnNames(resultColumns, headersByName, true)); + } + columnFilters.add(0, ColumnFilterData.eq("savings_account_id", savingsId.toString())); + if (resultColumns.isEmpty() && !queryRequest.hasResultColumn()) { + resultColumns.add(pkColumn); + selectColumns.add(pkColumn); + } + PageRequest pageable = pagedRequest.toPageable(); + PageRequest sortPageable; + if (pageable.getSort().isSorted()) { + List orders = pageable.getSort().toList(); + sortPageable = pageable.withSort(Sort.by(orders.stream() + .map(e -> e.withProperty(SearchUtil.validateToJdbcColumnName(e.getProperty(), headersByName, false))).toList())); + } else { + pageable = pageable.withSort(Sort.Direction.DESC, pkColumn); + sortPageable = pageable; + } + + String alias = "main"; + String dateFormat = pagedRequest.getDateFormat(); + String dateTimeFormat = pagedRequest.getDateTimeFormat(); + Locale locale = pagedRequest.getLocaleObject(); + StringBuilder select = new StringBuilder(sqlGenerator.buildSelect(selectColumns, alias, false)); + StringBuilder from = new StringBuilder(" ").append(sqlGenerator.buildFrom(apptable, alias, false)); + StringBuilder where = new StringBuilder(); + ArrayList params = new ArrayList<>(); + SearchUtil.buildQueryCondition(columnFilters, where, params, alias, headersByName, dateFormat, dateTimeFormat, locale, false, + sqlGenerator); + + if (datatableQueries != null) { + StringBuilder dataSelect = new StringBuilder(); + StringBuilder dataFrom = new StringBuilder(); + StringBuilder dataWhere = new StringBuilder(); + ArrayList dataParams = new ArrayList<>(); + for (int i = 0; i < datatableQueries.size(); i++) { + TableQueryData tableQuery = datatableQueries.get(i); + boolean added = datatableService.buildDataQueryEmbedded(EntityTables.SAVINGS_TRANSACTION, tableQuery.getTable(), + tableQuery.getQuery(), selectColumns, dataSelect, dataFrom, dataWhere, dataParams, alias, ("d" + i), dateFormat, + dateTimeFormat, locale); + if (added) { + if (!dataSelect.isEmpty()) { + select.append(select.isEmpty() ? "SELECT " : ", ").append(dataSelect); + } + if (!dataFrom.isEmpty()) { + from.append(" ").append(dataFrom); + } + if (!dataWhere.isEmpty()) { + where.append(where.isEmpty() ? " WHERE " : " AND ").append(dataWhere); + } + params.addAll(dataParams); + dataSelect.setLength(0); + dataFrom.setLength(0); + dataWhere.setLength(0); + dataParams.clear(); + } + resultColumns.addAll(tableQuery.getQuery().getNonNullResultColumns()); + } + } + + List results = new ArrayList<>(); + Object[] args = params.toArray(); + + // Execute the count Query + String countQuery = "SELECT COUNT(*)" + from + where; + Integer totalElements = jdbcTemplate.queryForObject(countQuery, Integer.class, args); + if (totalElements == null || totalElements == 0) { + return PageableExecutionUtils.getPage(results, pageable, () -> 0); + } + + StringBuilder query = new StringBuilder().append(select).append(from).append(where); + query.append(" ").append(sqlGenerator.buildOrderBy(sortPageable.getSort().toList(), null, false)); + if (pageable.isPaged()) { + query.append(" ").append(sqlGenerator.limit(pageable.getPageSize(), (int) pageable.getOffset())); + } + + SqlRowSet rowSet = jdbcTemplate.queryForRowSet(query.toString(), args); + + while (rowSet.next()) { + SearchUtil.extractJsonResult(rowSet, selectColumns, resultColumns, results); + } + return PageableExecutionUtils.getPage(results, pageable, () -> totalElements); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/data/AdvancedQueryData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/data/AdvancedQueryData.java new file mode 100644 index 000000000..b5a9a21c4 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/data/AdvancedQueryData.java @@ -0,0 +1,53 @@ +/** + * 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.data; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Immutable data object representing datatable data. + */ +@Data +@NoArgsConstructor +public final class AdvancedQueryData implements Serializable { + + private List columnFilters; + + private List resultColumns; + + public List getNonNullFilters() { + return columnFilters == null ? new ArrayList<>() : columnFilters; + } + + public boolean hasFilter() { + return columnFilters != null && !columnFilters.isEmpty(); + } + + public List getNonNullResultColumns() { + return resultColumns == null ? new ArrayList<>() : resultColumns; + } + + public boolean hasResultColumn() { + return resultColumns != null && !resultColumns.isEmpty(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/data/AdvancedQueryRequest.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/data/AdvancedQueryRequest.java new file mode 100644 index 000000000..4e5c1ef7b --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/data/AdvancedQueryRequest.java @@ -0,0 +1,46 @@ +/** + * 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.data; + +import java.io.Serializable; +import java.util.List; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Immutable data object representing datatable data. + */ +@Data +@NoArgsConstructor +public final class AdvancedQueryRequest implements Serializable { + + private AdvancedQueryData baseQuery; + + private List datatableQueries; + + public boolean hasFilter() { + return (baseQuery != null && baseQuery.hasFilter()) + || (datatableQueries != null && datatableQueries.stream().anyMatch(TableQueryData::hasFilter)); + } + + public boolean hasResultColumn() { + return (baseQuery != null && baseQuery.hasResultColumn()) + || (datatableQueries != null && datatableQueries.stream().anyMatch(TableQueryData::hasResultColumn)); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/data/TableQueryData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/data/TableQueryData.java new file mode 100644 index 000000000..fe682442c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/data/TableQueryData.java @@ -0,0 +1,43 @@ +/** + * 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.data; + +import java.io.Serializable; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Immutable data object representing datatable data. + */ +@Data +@NoArgsConstructor +public final class TableQueryData implements Serializable { + + private String table; + + private AdvancedQueryData query; + + public boolean hasFilter() { + return query != null && query.hasFilter(); + } + + public boolean hasResultColumn() { + return query != null && query.hasResultColumn(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/service/SearchUtil.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/service/SearchUtil.java index 2c3ac2211..880613f0d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/service/SearchUtil.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/search/service/SearchUtil.java @@ -25,7 +25,6 @@ import static org.apache.fineract.infrastructure.core.service.DateUtils.DEFAULT_ 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; @@ -38,6 +37,7 @@ import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.function.Predicate; import java.util.stream.Collectors; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; @@ -116,47 +116,75 @@ public final class SearchUtil { } @NotNull - public static List validateToJdbcColumns(List columns, Map columnHeaders, - List errors, boolean allowEmpty) { - List result = new ArrayList<>(); + public static List validateToJdbcColumnNames(List columns, Map headersByName, + boolean allowEmpty) { + List columnHeaders = validateToJdbcColumns(columns, headersByName, allowEmpty); + return columnHeaders.stream().map(e -> e == null ? null : e.getColumnName()).toList(); + } + + @NotNull + public static List validateToJdbcColumns(List columns, + Map headersByName, boolean allowEmpty) { + final List errors = new ArrayList<>(); + + 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))); + columns.forEach(rcn -> result.add(validateToJdbcColumnImpl(rcn, headersByName, errors, allowEmpty))); + } + if (!errors.isEmpty()) { + throw new PlatformApiDataValidationException(errors); } return result; } - public static String validateToJdbcColumn(String column, Map columnHeaders, - List errors, boolean allowEmpty) { + public static String validateToJdbcColumnName(String column, Map headersByName, boolean allowEmpty) { + ResultsetColumnHeaderData columnHeader = validateToJdbcColumn(column, headersByName, allowEmpty); + return columnHeader == null ? null : columnHeader.getColumnName(); + } + + public static ResultsetColumnHeaderData validateToJdbcColumn(String column, + @NotNull Map headersByName, boolean allowEmpty) { + final List errors = new ArrayList<>(); + ResultsetColumnHeaderData columnHeader = validateToJdbcColumnImpl(column, headersByName, errors, allowEmpty); + if (!errors.isEmpty()) { + throw new PlatformApiDataValidationException(errors); + } + return columnHeader; + } + + private static ResultsetColumnHeaderData validateToJdbcColumnImpl(String column, + @NotNull Map headersByName, @NotNull 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) { - 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)); - } - } + if (column == null) { + return null; } - return column; + Collection columnHeaders = headersByName.values(); + ResultsetColumnHeaderData columnHeader; + if ((columnHeader = findFiltered(columnHeaders, e -> e.isNamed(column))) == null + && (columnHeader = findFiltered(columnHeaders, e -> e.isNamed(column.replaceAll(" ", "_")))) == null + && (columnHeader = findFiltered(columnHeaders, e -> e.isNamed(camelToSnake(column.replaceAll(" ", ""))))) == null) { + errors.add(parameterErrorWithValue("error.msg.invalid.column", "Column not exist in database", API_PARAM_COLUMN, column)); + } + return columnHeader; } public static boolean buildQueryCondition(List columnFilters, @NotNull StringBuilder where, - @NotNull List params, String alias, Map columnHeaders, boolean embedded, - boolean strict, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) { + @NotNull List params, String alias, Map headersByName, String dateFormat, + String dateTimeFormat, Locale locale, boolean embedded, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) { if (columnFilters == null) { return false; } boolean added = false; int isize = columnFilters.size(); for (int i = 0; i < isize; i++) { - boolean addedFilter = buildFilterCondition(columnFilters.get(i), where, params, alias, columnHeaders, embedded, strict, - sqlGenerator); + boolean addedFilter = buildFilterCondition(columnFilters.get(i), where, params, alias, headersByName, dateFormat, + dateTimeFormat, locale, embedded, sqlGenerator); if (addedFilter && i < isize - 1) { where.append(" AND "); } @@ -166,8 +194,8 @@ public final class SearchUtil { } public static boolean buildFilterCondition(ColumnFilterData columnFilter, @NotNull StringBuilder where, @NotNull List params, - String alias, Map columnHeaders, boolean embedded, boolean strict, - @NotNull DatabaseSpecificSQLGenerator sqlGenerator) { + String alias, Map headersByName, String dateFormat, String dateTimeFormat, Locale locale, + boolean embedded, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) { String columnName = columnFilter.getColumn(); List filters = columnFilter.getFilters(); int size = filters.size(); @@ -175,26 +203,18 @@ public final class SearchUtil { if (!embedded && where.isEmpty()) { where.append(" WHERE "); } + ResultsetColumnHeaderData columnHeader = validateToJdbcColumn(columnName, headersByName, false); + FilterData filter = filters.get(i); SqlOperator operator = filter.getOperator(); List values = filter.getValues(); List objectValues = values == null ? null - : values.stream().map(e -> parseAndValidateJdbcColumnValue(columnName, e, columnHeaders, strict, sqlGenerator)) + : values.stream() + .map(e -> parseJdbcColumnValue(columnHeader, e, dateFormat, dateTimeFormat, locale, false, sqlGenerator)) .toList(); - operator.validateValues(values); - if (operator.isPlaceholderSupported()) { - where.append(operator.formatPlaceholder(sqlGenerator, columnName, alias)); - if (objectValues != null) { - if (operator.isListType()) { - params.add(objectValues); - } else { - params.addAll(objectValues); - } - } - } else { - where.append(operator.formatSql(sqlGenerator, columnHeaders.get(columnName).getColumnType(), columnName, alias, values)); - } + buildCondition(columnHeader.getColumnName(), columnHeader.getColumnType(), operator, objectValues, where, params, alias, + sqlGenerator); if (i < size - 1) { where.append(" AND "); } @@ -202,22 +222,24 @@ public final class SearchUtil { return size > 0; } - public static Object parseAndValidateJdbcColumnValue(String column, String columnValue, - Map columnHeaders, boolean strict, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) { - List errors = new ArrayList<>(); - column = validateToJdbcColumn(column, columnHeaders, errors, false); - if (!errors.isEmpty()) { - throw new PlatformApiDataValidationException(errors); + public static void buildCondition(@NotNull String definition, JdbcJavaType columnType, @NotNull SqlOperator operator, + List values, @NotNull StringBuilder where, @NotNull List params, String alias, + @NotNull DatabaseSpecificSQLGenerator sqlGenerator) { + int paramCount = values == null ? 0 : values.size(); + where.append(operator.formatPlaceholder(sqlGenerator, definition, paramCount, alias)); + if (values != null) { + params.addAll(values); } - ResultsetColumnHeaderData columnHeader = columnHeaders.get(column); - String dateFormat = columnHeader.getColumnType().isDateTimeType() ? DEFAULT_DATETIME_FORMAT : DEFAULT_DATE_FORMAT; - return columnHeader.getColumnType().toJdbcValue(sqlGenerator.getDialect(), - parseAndValidateColumnValue(columnHeader, columnValue, dateFormat, ENGLISH, strict, sqlGenerator), false); } - public static Object parseAndValidateColumnValue(final ResultsetColumnHeaderData columnHeader, final String pValue, - final String dateFormat, final Locale locale, boolean strict, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) { - String columnValue = pValue; + public static Object parseJdbcColumnValue(@NotNull ResultsetColumnHeaderData columnHeader, String columnValue, String dateFormat, + String dateTimeFormat, Locale locale, boolean strict, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) { + return columnHeader.getColumnType().toJdbcValue(sqlGenerator.getDialect(), + parseColumnValue(columnHeader, columnValue, dateFormat, dateTimeFormat, locale, strict, sqlGenerator), false); + } + + public static Object parseColumnValue(@NotNull ResultsetColumnHeaderData columnHeader, String columnValue, String dateFormat, + String dateTimeFormat, Locale locale, boolean strict, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) { JdbcJavaType colType = columnHeader.getColumnType(); if (!colType.isStringType() || !columnHeader.isMandatory()) { columnValue = StringUtils.trimToNull(columnValue); @@ -257,12 +279,14 @@ public final class SearchUtil { + " - Invalid Type " + colType.getJdbcName(sqlGenerator.getDialect()) + " (neither varchar nor int)"); } } - + locale = locale == null ? ENGLISH : locale; if (colType.isDateType()) { - return JsonParserHelper.convertFrom(columnValue, columnHeader.getColumnName(), dateFormat, locale); + String format = dateFormat == null ? DEFAULT_DATE_FORMAT : dateFormat; + return JsonParserHelper.convertFrom(columnValue, columnHeader.getColumnName(), format, locale); } if (colType.isDateTimeType()) { - return JsonParserHelper.convertDateTimeFrom(columnValue, columnHeader.getColumnName(), dateFormat, locale); + String format = dateTimeFormat == null ? DEFAULT_DATETIME_FORMAT : dateTimeFormat; + return JsonParserHelper.convertDateTimeFrom(columnValue, columnHeader.getColumnName(), format, locale); } if (colType.isAnyIntegerType()) { return helper.convertToInteger(columnValue, columnHeader.getColumnName(), locale); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java index 48d0ccb6d..db72cf6d8 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java @@ -23,6 +23,7 @@ import static org.apache.fineract.integrationtests.common.Utils.TENANT_PARAM_NAM import com.google.common.reflect.TypeToken; import com.google.gson.Gson; +import io.restassured.path.json.JsonPath; import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; import jakarta.ws.rs.core.HttpHeaders; @@ -39,6 +40,7 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import org.apache.fineract.client.models.PagedLocalRequestAdvancedQueryRequest; import org.apache.fineract.client.models.SavingsAccountTransactionsSearchResponse; import org.apache.fineract.client.util.JSON; import org.apache.fineract.integrationtests.client.IntegrationTest; @@ -661,6 +663,11 @@ public class SavingsAccountHelper extends IntegrationTest { return GSON.fromJson(response, SavingsAccountTransactionsSearchResponse.class); } + public Map querySavingsTransactions(Integer savingsId, PagedLocalRequestAdvancedQueryRequest request) { + String response = ok(fineract().savingsTransactions.advancedQuery1(savingsId.longValue(), request)); + return JsonPath.from(response).get(""); + } + public List getSavingsTransactions(final Integer savingsID) { final Object get = getSavingsCollectionAttribute(savingsID, "transactions"); final String json = new Gson().toJson(get); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/DatatableHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/DatatableHelper.java index 22eae4d56..5bdd75972 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/DatatableHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/DatatableHelper.java @@ -24,6 +24,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import com.google.gson.Gson; import com.google.gson.JsonElement; +import io.restassured.path.json.JsonPath; import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; import java.util.ArrayList; @@ -31,8 +32,10 @@ import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import org.apache.fineract.client.models.GetDataTablesResponse; +import org.apache.fineract.client.models.PagedLocalRequestAdvancedQueryData; import org.apache.fineract.client.models.PostDataTablesAppTableIdResponse; import org.apache.fineract.client.models.PostDataTablesRequest; import org.apache.fineract.client.models.PostDataTablesResponse; @@ -294,4 +297,8 @@ public class DatatableHelper extends IntegrationTest { return GSON.fromJson(response, PutDataTablesResponse.class); } + public Map queryDatatable(String dataTableName, PagedLocalRequestAdvancedQueryData request) { + String response = ok(fineract().dataTables.advancedQuery(dataTableName, request)); + return JsonPath.from(response).get(""); + } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableAdvancedQueryTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableAdvancedQueryTest.java new file mode 100644 index 000000000..5df73fb1d --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableAdvancedQueryTest.java @@ -0,0 +1,387 @@ +/** + * 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.integrationtests.datatable; + +import static org.apache.fineract.client.models.FilterData.OperatorEnum.BTW; +import static org.apache.fineract.client.models.FilterData.OperatorEnum.EQ; +import static org.apache.fineract.client.models.FilterData.OperatorEnum.GT; +import static org.apache.fineract.client.models.FilterData.OperatorEnum.GTE; +import static org.apache.fineract.client.models.FilterData.OperatorEnum.IN; +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_DECIMAL; +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_PARAM_APPTABLE_NAME; +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_MULTIROW; +import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.API_PARAM_SUBTYPE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.gson.Gson; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.builder.ResponseSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.fineract.client.models.AdvancedQueryData; +import org.apache.fineract.client.models.AdvancedQueryRequest; +import org.apache.fineract.client.models.ColumnFilterData; +import org.apache.fineract.client.models.FilterData; +import org.apache.fineract.client.models.GetDataTablesResponse; +import org.apache.fineract.client.models.PagedLocalRequestAdvancedQueryData; +import org.apache.fineract.client.models.PagedLocalRequestAdvancedQueryRequest; +import org.apache.fineract.client.models.PostDataTablesResponse; +import org.apache.fineract.client.models.ResultsetColumnHeaderData; +import org.apache.fineract.client.models.SortOrder; +import org.apache.fineract.client.models.TableQueryData; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.dataqueries.data.EntityTables; +import org.apache.fineract.integrationtests.common.BusinessDateHelper; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.CommonConstants; +import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper; +import org.apache.fineract.integrationtests.common.savings.SavingsProductHelper; +import org.apache.fineract.integrationtests.common.savings.SavingsStatusChecker; +import org.apache.fineract.integrationtests.common.system.DatatableHelper; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DatatableAdvancedQueryTest { + + private static final Logger LOG = LoggerFactory.getLogger(DatatableAdvancedQueryTest.class); + + private static final String SAVINGS_TRANSACTION_APP_TABLE_NAME = EntityTables.SAVINGS_TRANSACTION.getName(); + public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL"; + public static final String SAVINGS_DATE_FORMAT = "dd MMM yyyy"; + + private static final String COLUMN_STRING = "aString"; + private static final String COLUMN_TEXT = "aText"; + private static final String COLUMN_DATE = "aDate"; + private static final String COLUMN_BOOLEAN = "aBoolean"; + private static final String COLUMN_INTEGER = "aNumber"; + private static final String COLUMN_DECIMAL = "aDecimal"; + private static final String COLUMN_TRANSACTION_ID = "savings_transaction_id"; + private static final String COLUMN_ID = "id"; + private static final String COLUMN_SUBMITTED_DATE = "submitted_on_date"; + private static final String COLUMN_AMOUNT = "amount"; + + private RequestSpecification requestSpec; + private ResponseSpecification responseSpec; + private DatatableHelper datatableHelper; + private SavingsProductHelper savingsProductHelper; + private SavingsAccountHelper savingsAccountHelper; + + @BeforeEach + public void setup() { + Utils.initializeRESTAssured(); + requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + datatableHelper = new DatatableHelper(requestSpec, responseSpec); + savingsAccountHelper = new SavingsAccountHelper(requestSpec, responseSpec); + savingsProductHelper = new SavingsProductHelper(); + } + + @Test + public void testDatatableAdvancedQuery() { + String datatable = createAndVerifyDatatable(SAVINGS_TRANSACTION_APP_TABLE_NAME, null, false); + + LocalDate today = LocalDate.now(ZoneId.systemDefault()); + String todayS = DateUtils.format(today, SAVINGS_DATE_FORMAT); + LocalDate yesterday = today.minus(1, ChronoUnit.DAYS); + String yesterdayS = DateUtils.format(yesterday, SAVINGS_DATE_FORMAT); + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, true); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, today); + + final Integer clientId = ClientHelper.createClient(requestSpec, responseSpec, yesterdayS); + assertNotNull(clientId); + final Integer savingsId = createSavingsAccountDailyPosting(clientId, yesterdayS); + assertNotNull(savingsId); + + final Integer transactionIdD1 = (Integer) savingsAccountHelper.depositToSavingsAccount(savingsId, "100", yesterdayS, + CommonConstants.RESPONSE_RESOURCE_ID); + assertNotNull(transactionIdD1); + BigDecimal decValue1 = new BigDecimal("1.111"); + createDatatableEntry(datatable, transactionIdD1, yesterday, true, 1, decValue1); + final Integer transactionIdD2 = (Integer) savingsAccountHelper.depositToSavingsAccount(savingsId, "300", yesterdayS, + CommonConstants.RESPONSE_RESOURCE_ID); + assertNotNull(transactionIdD2); + createDatatableEntry(datatable, transactionIdD2, yesterday, false, 2, new BigDecimal("2.2")); + final Integer transactionIdW1 = (Integer) savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "100", todayS, + CommonConstants.RESPONSE_RESOURCE_ID); + assertNotNull(transactionIdW1); + createDatatableEntry(datatable, transactionIdW1, today, true, 3, new BigDecimal("3")); + + String yesterdayIsoS = DateUtils.format(yesterday, DateUtils.DEFAULT_DATE_FORMAT); + String todayIsoS = DateUtils.format(today, DateUtils.DEFAULT_DATE_FORMAT); + AdvancedQueryData query = new AdvancedQueryData() + .resultColumns(List.of(COLUMN_TRANSACTION_ID, COLUMN_STRING, COLUMN_TEXT, COLUMN_DATE, COLUMN_BOOLEAN, COLUMN_INTEGER, + COLUMN_DECIMAL)) + .addColumnFiltersItem(new ColumnFilterData().column(COLUMN_TEXT) + .addFiltersItem(new FilterData().operator(EQ).values(List.of(transactionIdD1.toString())))) + .addColumnFiltersItem(new ColumnFilterData().column(COLUMN_DATE) + .addFiltersItem(new FilterData().operator(BTW).values(List.of(yesterdayIsoS, yesterdayIsoS)))); + PagedLocalRequestAdvancedQueryData pagedQuery = new PagedLocalRequestAdvancedQueryData().page(0).size(3) + .addSortsItem(new SortOrder().property("created_at").direction(SortOrder.DirectionEnum.DESC)) + .addSortsItem(new SortOrder().property(COLUMN_TRANSACTION_ID).direction(SortOrder.DirectionEnum.DESC)).request(query); + Map response = datatableHelper.queryDatatable(datatable, pagedQuery); + + assertEquals(1, response.get("total")); + List content = (List) response.get("content"); + assertNotNull(content); + assertEquals(1, content.size()); + Map first = (Map) content.get(0); + assertEquals(transactionIdD1, first.get(COLUMN_TRANSACTION_ID)); + assertEquals(transactionIdD1.toString(), first.get(COLUMN_TEXT)); + assertEquals(yesterdayIsoS, first.get(COLUMN_DATE)); + assertTrue((Boolean) first.get(COLUMN_BOOLEAN)); + assertEquals(1, first.get(COLUMN_INTEGER)); + assertEquals(decValue1.floatValue(), first.get(COLUMN_DECIMAL)); + + query.resultColumns(List.of(COLUMN_TRANSACTION_ID)); + query.columnFilters(List.of( + new ColumnFilterData().column(COLUMN_INTEGER).addFiltersItem(new FilterData().operator(GTE).values(List.of("1"))), + new ColumnFilterData().column(COLUMN_DATE) + .addFiltersItem(new FilterData().operator(BTW).values(List.of(yesterdayIsoS, todayIsoS))))); + response = datatableHelper.queryDatatable(datatable, pagedQuery); + + assertEquals(3, response.get("total")); + content = (List) response.get("content"); + assertNotNull(content); + assertEquals(3, content.size()); + first = (Map) content.get(0); + assertEquals(transactionIdW1, first.get(COLUMN_TRANSACTION_ID)); + assertNull(first.get(COLUMN_TEXT)); + assertNull(first.get(COLUMN_DATE)); + assertNull(first.get(COLUMN_BOOLEAN)); + assertNull(first.get(COLUMN_INTEGER)); + assertNull(first.get(COLUMN_DECIMAL)); + assertEquals(transactionIdD2, ((Map) content.get(1)).get(COLUMN_TRANSACTION_ID)); + + deleteDatatable(datatable, transactionIdD1, transactionIdD2, transactionIdW1); + + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, false); + } + } + + @Test + public void testApptableWithDatatableAdvancedQuery() { + String datatable = createAndVerifyDatatable(SAVINGS_TRANSACTION_APP_TABLE_NAME, null, false); + + LocalDate today = LocalDate.now(ZoneId.systemDefault()); + String todayS = DateUtils.format(today, SAVINGS_DATE_FORMAT); + LocalDate yesterday = today.minus(1, ChronoUnit.DAYS); + String yesterdayS = DateUtils.format(yesterday, SAVINGS_DATE_FORMAT); + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, true); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, today); + + final Integer clientId = ClientHelper.createClient(requestSpec, responseSpec, yesterdayS); + assertNotNull(clientId); + final Integer savingsId = createSavingsAccountDailyPosting(clientId, yesterdayS); + assertNotNull(savingsId); + + final Integer transactionIdD1 = (Integer) savingsAccountHelper.depositToSavingsAccount(savingsId, "100", yesterdayS, + CommonConstants.RESPONSE_RESOURCE_ID); + assertNotNull(transactionIdD1); + BigDecimal decValue1 = new BigDecimal("1.111"); + createDatatableEntry(datatable, transactionIdD1, yesterday, true, 1, decValue1); + final Integer transactionIdD2 = (Integer) savingsAccountHelper.depositToSavingsAccount(savingsId, "300", yesterdayS, + CommonConstants.RESPONSE_RESOURCE_ID); + assertNotNull(transactionIdD2); + BigDecimal decValue2 = new BigDecimal("2.2"); + createDatatableEntry(datatable, transactionIdD2, yesterday, false, 2, decValue2); + final Integer transactionIdW1 = (Integer) savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "100", todayS, + CommonConstants.RESPONSE_RESOURCE_ID); + assertNotNull(transactionIdW1); + createDatatableEntry(datatable, transactionIdW1, today, true, 3, new BigDecimal("3")); + + String yesterdayIsoS = DateUtils.format(yesterday, DateUtils.DEFAULT_DATE_FORMAT); + String todayIsoS = DateUtils.format(today, DateUtils.DEFAULT_DATE_FORMAT); + + AdvancedQueryData baseQuery = new AdvancedQueryData().resultColumns(List.of(COLUMN_ID, COLUMN_SUBMITTED_DATE)) + .addColumnFiltersItem(new ColumnFilterData().column(COLUMN_AMOUNT) + .addFiltersItem(new FilterData().operator(GT).values(List.of("100")))) + .addColumnFiltersItem(new ColumnFilterData().column(COLUMN_SUBMITTED_DATE) + .addFiltersItem(new FilterData().operator(BTW).values(List.of(todayIsoS, todayIsoS)))); + AdvancedQueryData dataQuery = new AdvancedQueryData() + .resultColumns(List.of(COLUMN_TRANSACTION_ID, COLUMN_STRING, COLUMN_TEXT, COLUMN_DATE, COLUMN_BOOLEAN, COLUMN_INTEGER, + COLUMN_DECIMAL)) + .addColumnFiltersItem(new ColumnFilterData().column(COLUMN_TEXT).addFiltersItem( + new FilterData().operator(IN).values(List.of(transactionIdD1.toString(), transactionIdD2.toString())))) + .addColumnFiltersItem(new ColumnFilterData().column(COLUMN_DATE) + .addFiltersItem(new FilterData().operator(BTW).values(List.of(yesterdayIsoS, yesterdayIsoS)))); + AdvancedQueryRequest queryRequest = new AdvancedQueryRequest().baseQuery(baseQuery) + .datatableQueries(List.of(new TableQueryData().table(datatable).query(dataQuery))); + PagedLocalRequestAdvancedQueryRequest pagedRequest = new PagedLocalRequestAdvancedQueryRequest().page(0).size(2) + .addSortsItem(new SortOrder().property(COLUMN_SUBMITTED_DATE).direction(SortOrder.DirectionEnum.DESC)) + .addSortsItem(new SortOrder().property(COLUMN_ID).direction(SortOrder.DirectionEnum.DESC)).request(queryRequest); + Map response = savingsAccountHelper.querySavingsTransactions(savingsId, pagedRequest); + + assertEquals(1, response.get("total")); + List content = (List) response.get("content"); + assertNotNull(content); + assertEquals(1, content.size()); + Map first = (Map) content.get(0); + assertEquals(transactionIdD2, first.get(COLUMN_ID)); + assertEquals(todayIsoS, first.get(COLUMN_SUBMITTED_DATE)); + assertEquals(transactionIdD2, first.get(COLUMN_TRANSACTION_ID)); + assertEquals(transactionIdD2.toString(), first.get(COLUMN_TEXT)); + assertEquals(yesterdayIsoS, first.get(COLUMN_DATE)); + assertFalse((Boolean) first.get(COLUMN_BOOLEAN)); + assertEquals(2, first.get(COLUMN_INTEGER)); + assertEquals(decValue2.floatValue(), first.get(COLUMN_DECIMAL)); + + baseQuery.columnFilters(List.of( + new ColumnFilterData().column(COLUMN_AMOUNT).addFiltersItem(new FilterData().operator(GTE).values(List.of("100"))), + new ColumnFilterData().column(COLUMN_SUBMITTED_DATE) + .addFiltersItem(new FilterData().operator(BTW).values(List.of(todayIsoS, todayIsoS))))); + dataQuery.resultColumns(List.of(COLUMN_TRANSACTION_ID)); + dataQuery.columnFilters(List.of( + new ColumnFilterData().column(COLUMN_INTEGER).addFiltersItem(new FilterData().operator(GTE).values(List.of("1"))), + new ColumnFilterData().column(COLUMN_DATE) + .addFiltersItem(new FilterData().operator(BTW).values(List.of(yesterdayIsoS, todayIsoS))))); + response = savingsAccountHelper.querySavingsTransactions(savingsId, pagedRequest); + + assertEquals(3, response.get("total")); + content = (List) response.get("content"); + assertNotNull(content); + assertEquals(2, content.size()); // page size 2 + first = (Map) content.get(0); + assertEquals(transactionIdW1, first.get(COLUMN_ID)); + assertEquals(todayIsoS, first.get(COLUMN_SUBMITTED_DATE)); + assertEquals(transactionIdW1, first.get(COLUMN_TRANSACTION_ID)); + assertNull(first.get(COLUMN_TEXT)); + assertNull(first.get(COLUMN_DATE)); + assertNull(first.get(COLUMN_BOOLEAN)); + assertNull(first.get(COLUMN_INTEGER)); + assertNull(first.get(COLUMN_DECIMAL)); + assertEquals(transactionIdD2, ((Map) content.get(1)).get(COLUMN_TRANSACTION_ID)); + + deleteDatatable(datatable, transactionIdD1, transactionIdD2, transactionIdW1); + + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, false); + } + } + + private String createAndVerifyDatatable(String apptable, String subType, boolean multiRow) { + // creating datatable for apptable entity + final HashMap request = new HashMap<>(); + request.put(API_PARAM_DATATABLE_NAME, Utils.uniqueRandomStringGenerator("dt_" + apptable + "_", 5)); + request.put(API_PARAM_APPTABLE_NAME, apptable); + if (subType != null) { + request.put(API_PARAM_SUBTYPE, subType); + } + request.put(API_PARAM_MULTIROW, multiRow); + + final List> datatableColumns = new ArrayList<>(); + DatatableHelper.addDatatableColumnsWithUniqueAndIndex(datatableColumns, COLUMN_STRING, API_FIELD_TYPE_STRING, true, 50, null, + !multiRow, true); + DatatableHelper.addDatatableColumns(datatableColumns, COLUMN_TEXT, API_FIELD_TYPE_TEXT, false, null, null); + DatatableHelper.addDatatableColumns(datatableColumns, COLUMN_DATE, API_FIELD_TYPE_DATE, true, null, null); + DatatableHelper.addDatatableColumns(datatableColumns, COLUMN_BOOLEAN, API_FIELD_TYPE_BOOLEAN, false, null, null); + DatatableHelper.addDatatableColumns(datatableColumns, COLUMN_INTEGER, API_FIELD_TYPE_NUMBER, false, null, null); + DatatableHelper.addDatatableColumns(datatableColumns, COLUMN_DECIMAL, API_FIELD_TYPE_DECIMAL, false, null, null); + request.put(API_PARAM_COLUMNS, datatableColumns); + + String requestJson = new Gson().toJson(request); + LOG.info("map : {}", requestJson); + + PostDataTablesResponse response = datatableHelper.createDatatable(requestJson); + String datatable = response.getResourceIdentifier(); + assertNotNull(datatable); + GetDataTablesResponse dataTable = datatableHelper.getDataTableDetails(datatable); + List columnHeaderData = dataTable.getColumnHeaderData(); + assertNotNull(columnHeaderData); + // pk column and 2 audit columns were added automatically + assertEquals(9, columnHeaderData.size()); + return datatable; + } + + @NotNull + private HashMap createDatatableEntry(String datatable, Integer apptableId, LocalDate dateValue, Boolean boolValue, + Integer intValue, BigDecimal decValue) { + final HashMap request = new HashMap<>(); + request.put(COLUMN_STRING, Utils.uniqueRandomStringGenerator(apptableId.toString() + "_", 5)); + request.put(COLUMN_TEXT, apptableId); + request.put(COLUMN_DATE, DateUtils.format(dateValue, SAVINGS_DATE_FORMAT)); + request.put(COLUMN_BOOLEAN, boolValue); + request.put(COLUMN_INTEGER, intValue == null ? null : intValue.toString()); + request.put(COLUMN_DECIMAL, decValue == null ? null : decValue.toString()); + request.put("locale", "en"); + request.put("dateFormat", SAVINGS_DATE_FORMAT); + + String requestJson = new Gson().toJson(request); + HashMap response = datatableHelper.createDatatableEntry(datatable, apptableId, true, requestJson); + assertNotNull(response.get("resourceId")); + return response; + } + + private void deleteDatatable(String datatable, Integer... apptableIds) { + for (Integer apptableId : apptableIds) { + Integer deletedId = this.datatableHelper.deleteDatatableEntries(datatable, apptableId, "resourceId"); + assertEquals(apptableId, deletedId, "ERROR IN DELETING THE DATATABLE ENTRY"); + } + String deletedDatatable = this.datatableHelper.deleteDatatable(datatable); + assertEquals(datatable, deletedDatatable, "ERROR IN DELETING THE DATATABLE"); + } + + private Integer createSavingsProductDailyPosting() { + final String savingsProductJSON = savingsProductHelper.withInterestCompoundingPeriodTypeAsDaily() + .withInterestPostingPeriodTypeAsDaily().withInterestCalculationPeriodTypeAsDailyBalance().build(); + return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec); + } + + private Integer createSavingsAccountDailyPosting(final Integer clientID, final String startDate) { + final Integer savingsProductID = createSavingsProductDailyPosting(); + assertNotNull(savingsProductID); + final Integer savingsId = savingsAccountHelper.applyForSavingsApplicationOnDate(clientID, savingsProductID, ACCOUNT_TYPE_INDIVIDUAL, + startDate); + assertNotNull(savingsId); + HashMap savingsStatusHashMap = savingsAccountHelper.approveSavingsOnDate(savingsId, startDate); + SavingsStatusChecker.verifySavingsIsApproved(savingsStatusHashMap); + savingsStatusHashMap = savingsAccountHelper.activateSavingsAccount(savingsId, startDate); + SavingsStatusChecker.verifySavingsIsActive(savingsStatusHashMap); + return savingsId; + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableIntegrationTest.java index dbca64a63..3a0f6c5be 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableIntegrationTest.java @@ -456,83 +456,86 @@ public class DatatableIntegrationTest extends IntegrationTest { // creating new client datatable entry final boolean genericResultSet = true; - HashMap datatableEntryMap = new HashMap<>(); - datatableEntryMap.put("itsABoolean", null); - datatableEntryMap.put("itsADate", null); - datatableEntryMap.put("itsADatetime", null); - datatableEntryMap.put("itsADecimal", null); - datatableEntryMap.put("TST_TST_TST_cd_itsADropdown", null); - datatableEntryMap.put("itsANumber", null); - datatableEntryMap.put("itsAString", null); - datatableEntryMap.put("itsAText", null); + HashMap firstEntryMap = new HashMap<>(); + firstEntryMap.put("itsABoolean", null); + firstEntryMap.put("itsADate", null); + firstEntryMap.put("itsADatetime", null); + firstEntryMap.put("itsADecimal", null); + firstEntryMap.put("TST_TST_TST_cd_itsADropdown", null); + firstEntryMap.put("itsANumber", null); + firstEntryMap.put("itsAString", null); + firstEntryMap.put("itsAText", null); - datatableEntryMap.put("locale", "en"); - datatableEntryMap.put("dateFormat", "yyyy-MM-dd"); + firstEntryMap.put("locale", "en"); + firstEntryMap.put("dateFormat", "yyyy-MM-dd"); - String datatableEntryRequestJsonString = new GsonBuilder().serializeNulls().create().toJson(datatableEntryMap); - LOG.info("map : {}", datatableEntryRequestJsonString); + String firstEntryRequestJsonString = new GsonBuilder().serializeNulls().create().toJson(firstEntryMap); + LOG.info("map : {}", firstEntryRequestJsonString); - HashMap datatableEntryResponseFirst = this.datatableHelper.createDatatableEntry(datatableName, loanID, - genericResultSet, datatableEntryRequestJsonString); + HashMap firstEntryResponse = this.datatableHelper.createDatatableEntry(datatableName, loanID, genericResultSet, + firstEntryRequestJsonString); + assertNotNull(firstEntryResponse.get("resourceId"), "ERROR IN CREATING THE ENTITY DATATABLE RECORD"); - datatableEntryMap = new HashMap<>(); - datatableEntryMap.put("itsABoolean", ""); - datatableEntryMap.put("itsADate", ""); - datatableEntryMap.put("itsADatetime", ""); - datatableEntryMap.put("itsADecimal", ""); - datatableEntryMap.put("TST_TST_TST_cd_itsADropdown", ""); - datatableEntryMap.put("itsANumber", ""); - datatableEntryMap.put("itsAString", ""); - datatableEntryMap.put("itsAText", ""); + HashMap secondEntryMap = new HashMap<>(); + secondEntryMap.put("itsABoolean", ""); + secondEntryMap.put("itsADate", ""); + secondEntryMap.put("itsADatetime", ""); + secondEntryMap.put("itsADecimal", ""); + secondEntryMap.put("TST_TST_TST_cd_itsADropdown", ""); + secondEntryMap.put("itsANumber", ""); + secondEntryMap.put("itsAString", ""); + secondEntryMap.put("itsAText", ""); - datatableEntryMap.put("locale", "en"); - datatableEntryMap.put("dateFormat", "yyyy-MM-dd"); + secondEntryMap.put("locale", "en"); + secondEntryMap.put("dateFormat", "yyyy-MM-dd"); - datatableEntryRequestJsonString = new GsonBuilder().serializeNulls().create().toJson(datatableEntryMap); - HashMap datatableEntryResponseSecond = this.datatableHelper.createDatatableEntry(datatableName, loanID, - genericResultSet, datatableEntryRequestJsonString); - assertNotNull(datatableEntryResponseFirst.get("resourceId"), "ERROR IN CREATING THE ENTITY DATATABLE RECORD"); - assertNotNull(datatableEntryResponseSecond.get("resourceId"), "ERROR IN CREATING THE ENTITY DATATABLE RECORD"); + String secondEntryRequestJsonString = new GsonBuilder().serializeNulls().create().toJson(secondEntryMap); + HashMap secondEntryResponse = this.datatableHelper.createDatatableEntry(datatableName, loanID, genericResultSet, + secondEntryRequestJsonString); + assertNotNull(secondEntryResponse.get("resourceId"), "ERROR IN CREATING THE ENTITY DATATABLE RECORD"); // Read the Datatable entry generated with genericResultSet in true (default) HashMap items = this.datatableHelper.readDatatableEntry(datatableName, loanID, genericResultSet, null, ""); assertNotNull(items); assertEquals(2, ((List) items.get("data")).size()); - assertEquals("id", ((Map) ((List) items.get("columnHeaders")).get(0)).get("columnName")); - assertEquals(1, ((List) ((Map) ((List) items.get("data")).get(0)).get("row")).get(0)); - assertEquals("loan_id", ((Map) ((List) items.get("columnHeaders")).get(1)).get("columnName")); - assertEquals(loanID, ((List) ((Map) ((List) items.get("data")).get(0)).get("row")).get(1)); - assertEquals("itsABoolean", ((Map) ((List) items.get("columnHeaders")).get(2)).get("columnName")); - assertNull(((List) ((Map) ((List) items.get("data")).get(0)).get("row")).get(2)); - assertEquals("itsADate", ((Map) ((List) items.get("columnHeaders")).get(3)).get("columnName")); - assertNull(((List) ((Map) ((List) items.get("data")).get(0)).get("row")).get(3)); - assertEquals("itsADatetime", ((Map) ((List) items.get("columnHeaders")).get(4)).get("columnName")); - assertNull(((List) ((Map) ((List) items.get("data")).get(0)).get("row")).get(4)); - assertEquals("itsADecimal", ((Map) ((List) items.get("columnHeaders")).get(5)).get("columnName")); - assertNull(((List) ((Map) ((List) items.get("data")).get(0)).get("row")).get(5)); - assertEquals("TST_TST_TST_cd_itsADropdown", ((Map) ((List) items.get("columnHeaders")).get(6)).get("columnName")); - assertNull(((List) ((Map) ((List) items.get("data")).get(0)).get("row")).get(6)); - assertEquals("itsANumber", ((Map) ((List) items.get("columnHeaders")).get(7)).get("columnName")); - assertNull(((List) ((Map) ((List) items.get("data")).get(0)).get("row")).get(7)); - assertEquals("itsAString", ((Map) ((List) items.get("columnHeaders")).get(8)).get("columnName")); - assertNull(((List) ((Map) ((List) items.get("data")).get(0)).get("row")).get(8)); - assertEquals("itsAText", ((Map) ((List) items.get("columnHeaders")).get(9)).get("columnName")); - assertNull(((List) ((Map) ((List) items.get("data")).get(0)).get("row")).get(9)); + List headers = (List) items.get("columnHeaders"); + List firstEntryValues = (List) ((Map) ((List) items.get("data")).get(0)).get("row"); + assertEquals("id", ((Map) headers.get(0)).get("columnName")); + assertEquals(1, firstEntryValues.get(0)); + assertEquals("loan_id", ((Map) headers.get(1)).get("columnName")); + assertEquals(loanID, firstEntryValues.get(1)); + assertEquals("itsABoolean", ((Map) headers.get(2)).get("columnName")); + assertNull(firstEntryValues.get(2)); + assertEquals("itsADate", ((Map) headers.get(3)).get("columnName")); + assertNull(firstEntryValues.get(3)); + assertEquals("itsADatetime", ((Map) headers.get(4)).get("columnName")); + assertNull(firstEntryValues.get(4)); + assertEquals("itsADecimal", ((Map) headers.get(5)).get("columnName")); + assertNull(firstEntryValues.get(5)); + assertEquals("TST_TST_TST_cd_itsADropdown", ((Map) headers.get(6)).get("columnName")); + assertNull(firstEntryValues.get(6)); + assertEquals("itsANumber", ((Map) headers.get(7)).get("columnName")); + assertNull(firstEntryValues.get(7)); + assertEquals("itsAString", ((Map) headers.get(8)).get("columnName")); + assertNull(firstEntryValues.get(8)); + assertEquals("itsAText", ((Map) headers.get(9)).get("columnName")); + assertNull(firstEntryValues.get(9)); - assertEquals(2, ((List) ((Map) ((List) items.get("data")).get(1)).get("row")).get(0)); - assertEquals(loanID, ((List) ((Map) ((List) items.get("data")).get(1)).get("row")).get(1)); - assertNull(((List) ((Map) ((List) items.get("data")).get(1)).get("row")).get(2)); - assertNull(((List) ((Map) ((List) items.get("data")).get(1)).get("row")).get(3)); - assertNull(((List) ((Map) ((List) items.get("data")).get(1)).get("row")).get(4)); - assertNull(((List) ((Map) ((List) items.get("data")).get(1)).get("row")).get(5)); - assertNull(((List) ((Map) ((List) items.get("data")).get(1)).get("row")).get(6)); - assertNull(((List) ((Map) ((List) items.get("data")).get(1)).get("row")).get(7)); - assertNull(((List) ((Map) ((List) items.get("data")).get(1)).get("row")).get(8)); - assertNull(((List) ((Map) ((List) items.get("data")).get(1)).get("row")).get(9)); + List secondEntryValues = (List) ((Map) ((List) items.get("data")).get(1)).get("row"); + assertEquals(2, secondEntryValues.get(0)); + assertEquals(loanID, secondEntryValues.get(1)); + assertNull(secondEntryValues.get(2)); + assertNull(secondEntryValues.get(3)); + assertNull(secondEntryValues.get(4)); + assertNull(secondEntryValues.get(5)); + assertNull(secondEntryValues.get(6)); + assertNull(secondEntryValues.get(7)); + assertNull(secondEntryValues.get(8)); + assertNull(secondEntryValues.get(9)); PutDataTablesAppTableIdDatatableIdResponse updatedDatatableEntryResponse = this.datatableHelper.updateDatatableEntry(datatableName, - loanID, 1, datatableEntryRequestJsonString); + loanID, 1, secondEntryRequestJsonString); assertNotNull(updatedDatatableEntryResponse); assertEquals(0, updatedDatatableEntryResponse.getChanges().size()); }