FINERACT-1910 Support Pagination, sorting and filtering for Datatable
This commit is contained in:
+41
@@ -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<T> extends PagedRequest<T> {
|
||||||
|
|
||||||
|
private String dateFormat;
|
||||||
|
|
||||||
|
private String dateTimeFormat;
|
||||||
|
|
||||||
|
private String locale;
|
||||||
|
|
||||||
|
public Locale getLocaleObject() {
|
||||||
|
return locale == null ? null : JsonParserHelper.localeFromString(locale);
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -30,14 +30,14 @@ public enum JdbcJavaType {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object toJdbcValueImpl(@NotNull DatabaseType dialect, Object value) {
|
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")) { //
|
BOOLEAN(JavaType.BOOLEAN, new DialectType(JDBCType.BIT), new DialectType(JDBCType.BOOLEAN, null, "BOOL")) { //
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object toJdbcValueImpl(@NotNull DatabaseType dialect, Object value) {
|
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")), //
|
SMALLINT(JavaType.SHORT, new DialectType(JDBCType.SMALLINT, true), new DialectType(JDBCType.SMALLINT, null, "INT2")), //
|
||||||
|
|||||||
+48
-31
@@ -47,7 +47,7 @@ public enum SqlOperator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
return format("%s %s CONCAT('%%', %s, '%%')", definition, getSymbol(), placeholder);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -60,7 +60,7 @@ public enum SqlOperator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
return format("%s %s CONCAT('%%', %s, '%%')", definition, getSymbol(), placeholder);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -74,7 +74,7 @@ public enum SqlOperator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
return format("%s %s %s AND %s", definition, getSymbol(), placeholder, placeholder);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -88,7 +88,7 @@ public enum SqlOperator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
return format("%s %s %s AND %s", definition, getSymbol(), placeholder, placeholder);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -102,8 +102,13 @@ public enum SqlOperator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isPlaceholderSupported() {
|
protected String formatPlaceholderImpl(String definition, int paramCount, String placeholder) {
|
||||||
return false;
|
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) { //
|
NIN("NOT IN", -1) { //
|
||||||
@@ -116,8 +121,13 @@ public enum SqlOperator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isPlaceholderSupported() {
|
protected String formatPlaceholderImpl(String definition, int paramCount, String placeholder) {
|
||||||
return false;
|
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), //
|
NULL("IS NULL", 0), //
|
||||||
@@ -160,38 +170,45 @@ public enum SqlOperator {
|
|||||||
: format("%s %s %s", definition, symbol, sqlGenerator.formatValue(columnType, values[0]));
|
: format("%s %s %s", definition, symbol, sqlGenerator.formatValue(columnType, values[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPlaceholderSupported() {
|
public String formatNamedParam(@NotNull DatabaseSpecificSQLGenerator sqlGenerator, String definition, int paramCount, String alias) {
|
||||||
return true;
|
validateParamCount(paramCount);
|
||||||
}
|
if (paramCount > 1) {
|
||||||
|
throw new PlatformServiceUnavailableException("error.msg.database.operator.named.invalid",
|
||||||
public String formatPlaceholder(@NotNull DatabaseSpecificSQLGenerator sqlGenerator, String definition, String alias) {
|
"Named parameter is not allowed on " + this);
|
||||||
return formatPlaceholder(sqlGenerator, definition, alias, "?");
|
|
||||||
}
|
|
||||||
|
|
||||||
public String formatPlaceholder(@NotNull DatabaseSpecificSQLGenerator sqlGenerator, String definition, String alias,
|
|
||||||
String placeholder) {
|
|
||||||
return formatPlaceholderImpl(sqlGenerator.alias(sqlGenerator.escape(definition), alias), placeholder);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String formatPlaceholderImpl(String definition, String placeholder) {
|
|
||||||
if (!isPlaceholderSupported()) {
|
|
||||||
throw new UnsupportedOperationException("Placeholder is not supported for this operator");
|
|
||||||
}
|
}
|
||||||
|
return 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);
|
return paramCount == 0 ? format("%s %s", definition, symbol) : format("%s %s %s", definition, symbol, placeholder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validateValues(String... values) {
|
public void validateValues(String... values) {
|
||||||
if (values == null ? paramCount != 0 : (paramCount < 0 ? values.length < -paramCount : values.length != paramCount)) {
|
validateParamCount(values == null ? 0 : values.length);
|
||||||
throw new PlatformServiceUnavailableException("error.msg.database.operator.invalid",
|
|
||||||
"Number of parameters " + Arrays.toString(values) + " must be " + Math.abs(paramCount) + " on " + this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validateValues(List<?> values) {
|
public void validateValues(List<?> values) {
|
||||||
int size = values == null ? 0 : values.size();
|
validateParamCount(values == null ? 0 : values.size());
|
||||||
if (paramCount < 0 ? size < -paramCount : size != paramCount) {
|
}
|
||||||
|
|
||||||
|
public void validateParamCount(int paramCount) {
|
||||||
|
if (this.paramCount < 0 ? paramCount < -this.paramCount : paramCount != this.paramCount) {
|
||||||
throw new PlatformServiceUnavailableException("error.msg.database.operator.invalid",
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+39
@@ -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<ColumnFilter> columnFilters;
|
||||||
|
|
||||||
|
private List<String> resultColumns;
|
||||||
|
|
||||||
|
private String datatable;
|
||||||
|
|
||||||
|
}
|
||||||
+16
@@ -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.CommandProcessingResult;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
|
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.DatatableData;
|
||||||
import org.apache.fineract.infrastructure.dataqueries.data.GenericResultsetData;
|
import org.apache.fineract.infrastructure.dataqueries.data.GenericResultsetData;
|
||||||
import org.apache.fineract.infrastructure.dataqueries.service.GenericDataService;
|
import org.apache.fineract.infrastructure.dataqueries.service.GenericDataService;
|
||||||
import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService;
|
import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService;
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
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;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Path("/v1/datatables")
|
@Path("/v1/datatables")
|
||||||
@@ -225,6 +228,19 @@ public class DatatablesApiResource {
|
|||||||
return this.toApiJsonSerializer.serializePretty(ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters()), result);
|
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<AdvancedQueryData> queryRequest, @Context final UriInfo uriInfo) {
|
||||||
|
final Page<JsonObject> result = this.readWriteNonCoreDataService.queryDataTableAdvanced(datatable, queryRequest);
|
||||||
|
return this.toApiJsonSerializer.serializePretty(ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters()), result);
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("{datatable}/{apptableId}")
|
@Path("{datatable}/{apptableId}")
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
|||||||
+63
-4
@@ -18,8 +18,16 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.infrastructure.dataqueries.data;
|
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.JsonElement;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@@ -29,9 +37,13 @@ import java.util.Map;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
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.core.serialization.FromJsonHelper;
|
||||||
import org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant;
|
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.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -64,9 +76,56 @@ public class DataTableValidator {
|
|||||||
baseDataValidator.reset().parameter(DataTableApiConstant.categoryParamName).value(category).isOneOfTheseValues(objectArray);
|
baseDataValidator.reset().parameter(DataTableApiConstant.categoryParamName).value(category).isOneOfTheseValues(objectArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dataValidationErrors.isEmpty()) {
|
baseDataValidator.throwValidationErrors();
|
||||||
throw new PlatformApiDataValidationException(dataValidationErrors);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
public void validateTableSearch(@NotNull AdvancedQueryRequest queryRequest) {
|
||||||
|
final List<ApiParameterError> 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<TableQueryData> 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<ColumnFilterData> columnFilters = queryData.getColumnFilters();
|
||||||
|
if (columnFilters != null) {
|
||||||
|
for (ColumnFilterData columnFilter : columnFilters) {
|
||||||
|
validator.reset().parameter(API_PARAM_COLUMN).value(columnFilter.getColumn()).notNull();
|
||||||
|
List<FilterData> 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<String> resultColumns = queryData.getResultColumns();
|
||||||
|
if (resultColumns != null) {
|
||||||
|
for (String resultColumn : resultColumns) {
|
||||||
|
validator.reset().parameter(API_PARAM_RESULTCOLUMNS).value(resultColumn).notBlank();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-2
@@ -56,15 +56,20 @@ public enum EntityTables {
|
|||||||
private static final Map<String, EntityTables> BY_ENTITY_NAME = Arrays.stream(VALUES)
|
private static final Map<String, EntityTables> BY_ENTITY_NAME = Arrays.stream(VALUES)
|
||||||
.collect(Collectors.toMap(EntityTables::getName, e -> e));
|
.collect(Collectors.toMap(EntityTables::getName, e -> e));
|
||||||
|
|
||||||
|
@NotNull
|
||||||
private final String name;
|
private final String name;
|
||||||
|
@NotNull
|
||||||
private final String apptableName;
|
private final String apptableName;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
private final String foreignKeyColumnNameOnDatatable;
|
private final String foreignKeyColumnNameOnDatatable;
|
||||||
|
@NotNull
|
||||||
private final String refColumn; // referenced column name on apptable
|
private final String refColumn; // referenced column name on apptable
|
||||||
|
|
||||||
private final ImmutableList<StatusEnum> checkStatuses;
|
private final ImmutableList<StatusEnum> 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.name = name;
|
||||||
this.apptableName = apptableName;
|
this.apptableName = apptableName;
|
||||||
this.foreignKeyColumnNameOnDatatable = foreignKeyColumnNameOnDatatable;
|
this.foreignKeyColumnNameOnDatatable = foreignKeyColumnNameOnDatatable;
|
||||||
@@ -72,22 +77,26 @@ public enum EntityTables {
|
|||||||
this.checkStatuses = statuses == null ? ImmutableList.of() : ImmutableList.copyOf(statuses);
|
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);
|
this(name, name, foreignKeyColumnNameOnDatatable, refColumn, statuses);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
public String getApptableName() {
|
public String getApptableName() {
|
||||||
return apptableName;
|
return apptableName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
public String getForeignKeyColumnNameOnDatatable() {
|
public String getForeignKeyColumnNameOnDatatable() {
|
||||||
return this.foreignKeyColumnNameOnDatatable;
|
return this.foreignKeyColumnNameOnDatatable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
public String getRefColumn() {
|
public String getRefColumn() {
|
||||||
return refColumn;
|
return refColumn;
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -21,10 +21,15 @@ package org.apache.fineract.infrastructure.dataqueries.service;
|
|||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.PagedLocalRequest;
|
||||||
import org.apache.fineract.infrastructure.dataqueries.data.DatatableData;
|
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.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;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
|
||||||
public interface ReadWriteNonCoreDataService {
|
public interface ReadWriteNonCoreDataService {
|
||||||
@@ -75,4 +80,10 @@ public interface ReadWriteNonCoreDataService {
|
|||||||
|
|
||||||
List<JsonObject> queryDataTable(@NotNull String datatable, @NotNull String columnName, String columnValue,
|
List<JsonObject> queryDataTable(@NotNull String datatable, @NotNull String columnName, String columnValue,
|
||||||
@NotNull String resultColumns);
|
@NotNull String resultColumns);
|
||||||
|
|
||||||
|
Page<JsonObject> queryDataTableAdvanced(@NotNull String datatable, @NotNull PagedLocalRequest<AdvancedQueryData> pagedRequest);
|
||||||
|
|
||||||
|
boolean buildDataQueryEmbedded(@NotNull EntityTables entityTable, @NotNull String datatable, @NotNull AdvancedQueryData request,
|
||||||
|
@NotNull List<String> selectColumns, @NotNull StringBuilder select, @NotNull StringBuilder from, @NotNull StringBuilder where,
|
||||||
|
@NotNull List<Object> params, String mainAlias, String alias, String dateFormat, String dateTimeFormat, Locale locale);
|
||||||
}
|
}
|
||||||
|
|||||||
+120
-13
@@ -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.DatatableCommandFromApiJsonDeserializer;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
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.DatabaseSpecificSQLGenerator;
|
||||||
import org.apache.fineract.infrastructure.core.service.database.DatabaseType;
|
import org.apache.fineract.infrastructure.core.service.database.DatabaseType;
|
||||||
import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver;
|
import org.apache.fineract.infrastructure.core.service.database.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.service.SqlInjectionPreventerService;
|
||||||
import org.apache.fineract.infrastructure.security.utils.ColumnValidator;
|
import org.apache.fineract.infrastructure.security.utils.ColumnValidator;
|
||||||
import org.apache.fineract.infrastructure.security.utils.SQLInjectionValidator;
|
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.portfolio.search.service.SearchUtil;
|
||||||
import org.apache.fineract.useradministration.domain.AppUser;
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
import org.springframework.dao.DataAccessException;
|
import org.springframework.dao.DataAccessException;
|
||||||
import org.springframework.dao.DataIntegrityViolationException;
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
import org.springframework.dao.EmptyResultDataAccessException;
|
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.JdbcTemplate;
|
||||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
||||||
import org.springframework.jdbc.support.GeneratedKeyHolder;
|
import org.springframework.jdbc.support.GeneratedKeyHolder;
|
||||||
@@ -199,20 +206,16 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
|
|||||||
public List<JsonObject> queryDataTable(@NotNull String datatable, @NotNull String columnName, String columnValueString,
|
public List<JsonObject> queryDataTable(@NotNull String datatable, @NotNull String columnName, String columnValueString,
|
||||||
@NotNull String resultColumnsString) {
|
@NotNull String resultColumnsString) {
|
||||||
datatable = validateDatatableRegistered(datatable);
|
datatable = validateDatatableRegistered(datatable);
|
||||||
Map<String, ResultsetColumnHeaderData> columnHeaders = SearchUtil
|
Map<String, ResultsetColumnHeaderData> headersByName = SearchUtil
|
||||||
.mapHeadersToName(genericDataService.fillResultsetColumnHeaders(datatable));
|
.mapHeadersToName(genericDataService.fillResultsetColumnHeaders(datatable));
|
||||||
|
|
||||||
List<ApiParameterError> errors = new ArrayList<>();
|
|
||||||
columnName = SearchUtil.validateToJdbcColumn(columnName, columnHeaders, errors, false);
|
|
||||||
List<String> resultColumns = asList(resultColumnsString.split(","));
|
List<String> resultColumns = asList(resultColumnsString.split(","));
|
||||||
List<String> selectColumns = SearchUtil.validateToJdbcColumns(resultColumns, columnHeaders, errors, false);
|
List<String> selectColumns = SearchUtil.validateToJdbcColumnNames(resultColumns, headersByName, false);
|
||||||
if (!errors.isEmpty()) {
|
ResultsetColumnHeaderData column = SearchUtil.validateToJdbcColumn(columnName, headersByName, false);
|
||||||
throw new PlatformApiDataValidationException(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 "
|
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);
|
SqlRowSet rowSet = jdbcTemplate.queryForRowSet(sql, columnValue);
|
||||||
|
|
||||||
List<JsonObject> results = new ArrayList<>();
|
List<JsonObject> results = new ArrayList<>();
|
||||||
@@ -222,6 +225,110 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Page<JsonObject> queryDataTableAdvanced(@NotNull String datatable, @NotNull PagedLocalRequest<AdvancedQueryData> pagedRequest) {
|
||||||
|
datatable = validateDatatableRegistered(datatable);
|
||||||
|
context.authenticatedUser().validateHasDatatableReadPermission(datatable);
|
||||||
|
|
||||||
|
AdvancedQueryData request = pagedRequest.getRequest().orElseThrow();
|
||||||
|
dataTableValidator.validateTableSearch(request);
|
||||||
|
|
||||||
|
Map<String, ResultsetColumnHeaderData> headersByName = SearchUtil
|
||||||
|
.mapHeadersToName(genericDataService.fillResultsetColumnHeaders(datatable));
|
||||||
|
String pkColumn = SearchUtil.getFiltered(headersByName.values(), ResultsetColumnHeaderData::getIsColumnPrimaryKey).getColumnName();
|
||||||
|
|
||||||
|
List<ColumnFilterData> columnFilters = request.getNonNullFilters();
|
||||||
|
columnFilters.forEach(e -> e.setColumn(SearchUtil.validateToJdbcColumnName(e.getColumn(), headersByName, false)));
|
||||||
|
|
||||||
|
List<String> resultColumns = request.getNonNullResultColumns();
|
||||||
|
List<String> 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<Sort.Order> 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<Object> params = new ArrayList<>();
|
||||||
|
SearchUtil.buildQueryCondition(columnFilters, where, params, null, headersByName, dateFormat, dateTimeFormat, locale, false,
|
||||||
|
sqlGenerator);
|
||||||
|
|
||||||
|
List<JsonObject> 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<String> selectColumns, @NotNull StringBuilder select, @NotNull StringBuilder from, @NotNull StringBuilder where,
|
||||||
|
@NotNull List<Object> params, String mainAlias, String alias, String dateFormat, String dateTimeFormat, Locale locale) {
|
||||||
|
List<String> resultColumns = request.getResultColumns();
|
||||||
|
List<ColumnFilterData> columnFilters = request.getColumnFilters();
|
||||||
|
if ((resultColumns == null || resultColumns.isEmpty()) && (columnFilters == null || columnFilters.isEmpty())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
datatable = validateDatatableRegistered(datatable);
|
||||||
|
context.authenticatedUser().validateHasDatatableReadPermission(datatable);
|
||||||
|
|
||||||
|
Map<String, ResultsetColumnHeaderData> headersByName = SearchUtil
|
||||||
|
.mapHeadersToName(genericDataService.fillResultsetColumnHeaders(datatable));
|
||||||
|
|
||||||
|
List<String> 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) {
|
private void logAsErrorUnexpectedDataIntegrityException(final Exception dve) {
|
||||||
log.error("Error occurred.", dve);
|
log.error("Error occurred.", dve);
|
||||||
}
|
}
|
||||||
@@ -1830,7 +1937,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
|
|||||||
columnHeaderUnderscored = this.genericDataService.replace(columnHeader.getColumnName(), space, underscore);
|
columnHeaderUnderscored = this.genericDataService.replace(columnHeader.getColumnName(), space, underscore);
|
||||||
if (queryParamColumnUnderscored.equalsIgnoreCase(columnHeaderUnderscored)) {
|
if (queryParamColumnUnderscored.equalsIgnoreCase(columnHeaderUnderscored)) {
|
||||||
pValue = queryParams.get(key);
|
pValue = queryParams.get(key);
|
||||||
validatedValue = SearchUtil.parseAndValidateColumnValue(columnHeader, pValue, dateFormat,
|
validatedValue = SearchUtil.parseColumnValue(columnHeader, pValue, dateFormat, dateFormat,
|
||||||
clientApplicationLocale, true, sqlGenerator);
|
clientApplicationLocale, true, sqlGenerator);
|
||||||
affectedColumns.put(columnHeader.getColumnName(), validatedValue);
|
affectedColumns.put(columnHeader.getColumnName(), validatedValue);
|
||||||
notFound = false;
|
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"
|
+ " JOIN m_entity_datatable_check edc ON edc.x_registered_table_name = xrt.registered_table_name"
|
||||||
+ " WHERE edc.x_registered_table_name = '" + datatableName + "'";
|
+ " WHERE edc.x_registered_table_name = '" + datatableName + "'";
|
||||||
final Long count = this.jdbcTemplate.queryForObject(sql, Long.class);
|
final Long count = this.jdbcTemplate.queryForObject(sql, Long.class);
|
||||||
return count > 0;
|
return count != null && count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- DbUtils ---
|
// --- DbUtils ---
|
||||||
@@ -1940,8 +2047,8 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
|
|||||||
|
|
||||||
private boolean isRegisteredDataTable(final String datatable) {
|
private boolean isRegisteredDataTable(final String datatable) {
|
||||||
final String sql = "SELECT COUNT(application_table_name) FROM " + TABLE_REGISTERED_TABLE + " WHERE registered_table_name = ?";
|
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);
|
final Integer count = jdbcTemplate.queryForObject(sql, Integer.class, datatable);
|
||||||
return count > 0;
|
return count != null && count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateDataTableExists(final String datatableName) {
|
private void validateDataTableExists(final String datatableName) {
|
||||||
|
|||||||
+18
@@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.savings.api;
|
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.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
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 jakarta.ws.rs.core.UriInfo;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||||
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||||
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
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.api.ApiRequestParameterHelper;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
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.ApiRequestJsonSerializationSettings;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper;
|
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.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData;
|
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData;
|
||||||
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadPlatformService;
|
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.data.SavingsAccountTransactionData;
|
||||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService;
|
import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.savings.service.search.SavingsAccountTransactionSearchService;
|
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.apache.fineract.portfolio.search.data.TransactionSearchRequest;
|
||||||
import org.springframework.dao.CannotAcquireLockException;
|
import org.springframework.dao.CannotAcquireLockException;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
@@ -159,6 +164,19 @@ public class SavingsAccountTransactionsApiResource {
|
|||||||
return toApiJsonSerializer.serialize(transactionsData);
|
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<AdvancedQueryRequest> queryRequest, @Context final UriInfo uriInfo) {
|
||||||
|
final Page<JsonObject> result = transactionsSearchService.queryAdvanced(savingsId, queryRequest);
|
||||||
|
return this.toApiJsonSerializer.serializePretty(ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters()), result);
|
||||||
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
|||||||
+5
@@ -18,12 +18,17 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.savings.service.search;
|
package org.apache.fineract.portfolio.savings.service.search;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
import jakarta.validation.constraints.NotNull;
|
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.savings.data.SavingsAccountTransactionData;
|
||||||
|
import org.apache.fineract.portfolio.search.data.AdvancedQueryRequest;
|
||||||
import org.apache.fineract.portfolio.search.data.TransactionSearchRequest;
|
import org.apache.fineract.portfolio.search.data.TransactionSearchRequest;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
|
|
||||||
public interface SavingsAccountTransactionSearchService {
|
public interface SavingsAccountTransactionSearchService {
|
||||||
|
|
||||||
Page<SavingsAccountTransactionData> searchTransactions(@NotNull Long savingsId, @NotNull TransactionSearchRequest searchParameters);
|
Page<SavingsAccountTransactionData> searchTransactions(@NotNull Long savingsId, @NotNull TransactionSearchRequest searchParameters);
|
||||||
|
|
||||||
|
Page<JsonObject> queryAdvanced(@NotNull Long savingsId, @NotNull PagedLocalRequest<AdvancedQueryRequest> pagedRequest);
|
||||||
}
|
}
|
||||||
|
|||||||
+124
-10
@@ -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 static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import lombok.RequiredArgsConstructor;
|
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.DateUtils;
|
||||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
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.DatabaseSpecificSQLGenerator;
|
||||||
import org.apache.fineract.infrastructure.core.service.database.SqlOperator;
|
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.EntityTables;
|
||||||
import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData;
|
import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData;
|
||||||
import org.apache.fineract.infrastructure.dataqueries.service.GenericDataService;
|
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.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
|
import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
|
||||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
|
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
|
||||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformServiceImpl;
|
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.ColumnFilterData;
|
||||||
|
import org.apache.fineract.portfolio.search.data.TableQueryData;
|
||||||
import org.apache.fineract.portfolio.search.data.TransactionSearchRequest;
|
import org.apache.fineract.portfolio.search.data.TransactionSearchRequest;
|
||||||
import org.apache.fineract.portfolio.search.service.SearchUtil;
|
import org.apache.fineract.portfolio.search.service.SearchUtil;
|
||||||
import org.jetbrains.annotations.Nullable;
|
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.domain.Sort;
|
||||||
import org.springframework.data.support.PageableExecutionUtils;
|
import org.springframework.data.support.PageableExecutionUtils;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.jdbc.support.rowset.SqlRowSet;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -60,6 +67,8 @@ public class SavingsAccountTransactionsSearchServiceImpl implements SavingsAccou
|
|||||||
private final PlatformSecurityContext context;
|
private final PlatformSecurityContext context;
|
||||||
private final GenericDataService genericDataService;
|
private final GenericDataService genericDataService;
|
||||||
private final DatabaseSpecificSQLGenerator sqlGenerator;
|
private final DatabaseSpecificSQLGenerator sqlGenerator;
|
||||||
|
private final ReadWriteNonCoreDataServiceImpl datatableService;
|
||||||
|
private final DataTableValidator dataTableValidator;
|
||||||
private final JdbcTemplate jdbcTemplate;
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -68,19 +77,15 @@ public class SavingsAccountTransactionsSearchServiceImpl implements SavingsAccou
|
|||||||
context.authenticatedUser().validateHasReadPermission(SAVINGS_ACCOUNT_RESOURCE_NAME);
|
context.authenticatedUser().validateHasReadPermission(SAVINGS_ACCOUNT_RESOURCE_NAME);
|
||||||
|
|
||||||
String apptable = EntityTables.SAVINGS_TRANSACTION.getApptableName();
|
String apptable = EntityTables.SAVINGS_TRANSACTION.getApptableName();
|
||||||
Map<String, ResultsetColumnHeaderData> columnHeaders = SearchUtil
|
Map<String, ResultsetColumnHeaderData> headersByName = SearchUtil
|
||||||
.mapHeadersToName(genericDataService.fillResultsetColumnHeaders(apptable));
|
.mapHeadersToName(genericDataService.fillResultsetColumnHeaders(apptable));
|
||||||
|
|
||||||
PageRequest pageable = searchParameters.getPageable();
|
PageRequest pageable = searchParameters.getPageable();
|
||||||
PageRequest sortPageable;
|
PageRequest sortPageable;
|
||||||
if (pageable.getSort().isSorted()) {
|
if (pageable.getSort().isSorted()) {
|
||||||
List<ApiParameterError> errors = new ArrayList<>();
|
|
||||||
List<Sort.Order> orders = pageable.getSort().toList();
|
List<Sort.Order> orders = pageable.getSort().toList();
|
||||||
sortPageable = pageable.withSort(Sort.by(orders.stream()
|
sortPageable = pageable.withSort(Sort.by(orders.stream()
|
||||||
.map(e -> e.withProperty(SearchUtil.validateToJdbcColumn(e.getProperty(), columnHeaders, errors, false))).toList()));
|
.map(e -> e.withProperty(SearchUtil.validateToJdbcColumnName(e.getProperty(), headersByName, false))).toList()));
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
throw new PlatformApiDataValidationException(errors);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
pageable = pageable.withSort(Sort.Direction.DESC, "transaction_date", "created_date", "id");
|
pageable = pageable.withSort(Sort.Direction.DESC, "transaction_date", "created_date", "id");
|
||||||
sortPageable = pageable;
|
sortPageable = pageable;
|
||||||
@@ -102,9 +107,9 @@ public class SavingsAccountTransactionsSearchServiceImpl implements SavingsAccou
|
|||||||
}
|
}
|
||||||
|
|
||||||
String alias = "tr";
|
String alias = "tr";
|
||||||
StringBuilder where = new StringBuilder(" WHERE ");
|
StringBuilder where = new StringBuilder();
|
||||||
ArrayList<Object> params = new ArrayList<>();
|
ArrayList<Object> 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();
|
SavingsAccountReadPlatformServiceImpl.SavingsAccountTransactionsMapper tm = new SavingsAccountReadPlatformServiceImpl.SavingsAccountTransactionsMapper();
|
||||||
Object[] args = params.toArray();
|
Object[] args = params.toArray();
|
||||||
@@ -171,4 +176,113 @@ public class SavingsAccountTransactionsSearchServiceImpl implements SavingsAccou
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Page<JsonObject> queryAdvanced(@NotNull Long savingsId, @NotNull PagedLocalRequest<AdvancedQueryRequest> pagedRequest) {
|
||||||
|
context.authenticatedUser().validateHasReadPermission(SAVINGS_ACCOUNT_RESOURCE_NAME);
|
||||||
|
String apptable = EntityTables.SAVINGS_TRANSACTION.getApptableName();
|
||||||
|
|
||||||
|
AdvancedQueryRequest queryRequest = pagedRequest.getRequest().orElseThrow();
|
||||||
|
dataTableValidator.validateTableSearch(queryRequest);
|
||||||
|
|
||||||
|
List<ResultsetColumnHeaderData> columnHeaders = genericDataService.fillResultsetColumnHeaders(apptable);
|
||||||
|
Map<String, ResultsetColumnHeaderData> headersByName = SearchUtil.mapHeadersToName(columnHeaders);
|
||||||
|
String pkColumn = SearchUtil.getFiltered(columnHeaders, ResultsetColumnHeaderData::getIsColumnPrimaryKey).getColumnName();
|
||||||
|
|
||||||
|
AdvancedQueryData baseQuery = queryRequest.getBaseQuery();
|
||||||
|
List<TableQueryData> datatableQueries = queryRequest.getDatatableQueries();
|
||||||
|
|
||||||
|
List<ColumnFilterData> columnFilters;
|
||||||
|
List<String> resultColumns;
|
||||||
|
List<String> 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<Sort.Order> 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<Object> 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<Object> 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<JsonObject> 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+53
@@ -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<ColumnFilterData> columnFilters;
|
||||||
|
|
||||||
|
private List<String> resultColumns;
|
||||||
|
|
||||||
|
public List<ColumnFilterData> getNonNullFilters() {
|
||||||
|
return columnFilters == null ? new ArrayList<>() : columnFilters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasFilter() {
|
||||||
|
return columnFilters != null && !columnFilters.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getNonNullResultColumns() {
|
||||||
|
return resultColumns == null ? new ArrayList<>() : resultColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasResultColumn() {
|
||||||
|
return resultColumns != null && !resultColumns.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
@@ -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<TableQueryData> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+76
-52
@@ -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.infrastructure.dataqueries.api.DataTableApiConstant.API_FIELD_MANDATORY;
|
||||||
import static org.apache.fineract.portfolio.search.SearchConstants.API_PARAM_COLUMN;
|
import static org.apache.fineract.portfolio.search.SearchConstants.API_PARAM_COLUMN;
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
@@ -38,6 +37,7 @@ import java.util.Collection;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import org.apache.commons.lang3.BooleanUtils;
|
import org.apache.commons.lang3.BooleanUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
@@ -116,47 +116,75 @@ public final class SearchUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public static List<String> validateToJdbcColumns(List<String> columns, Map<String, ResultsetColumnHeaderData> columnHeaders,
|
public static List<String> validateToJdbcColumnNames(List<String> columns, Map<String, ResultsetColumnHeaderData> headersByName,
|
||||||
List<ApiParameterError> errors, boolean allowEmpty) {
|
boolean allowEmpty) {
|
||||||
List<String> result = new ArrayList<>();
|
List<ResultsetColumnHeaderData> columnHeaders = validateToJdbcColumns(columns, headersByName, allowEmpty);
|
||||||
|
return columnHeaders.stream().map(e -> e == null ? null : e.getColumnName()).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public static List<ResultsetColumnHeaderData> validateToJdbcColumns(List<String> columns,
|
||||||
|
Map<String, ResultsetColumnHeaderData> headersByName, boolean allowEmpty) {
|
||||||
|
final List<ApiParameterError> errors = new ArrayList<>();
|
||||||
|
|
||||||
|
List<ResultsetColumnHeaderData> result = new ArrayList<>();
|
||||||
if (columns == null || columns.isEmpty()) {
|
if (columns == null || columns.isEmpty()) {
|
||||||
if (!allowEmpty) {
|
if (!allowEmpty) {
|
||||||
errors.add(parameterErrorWithValue("error.msg.columns.empty", "Columns list is empty", API_PARAM_COLUMN, null));
|
errors.add(parameterErrorWithValue("error.msg.columns.empty", "Columns list is empty", API_PARAM_COLUMN, null));
|
||||||
}
|
}
|
||||||
} else {
|
} 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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String validateToJdbcColumn(String column, Map<String, ResultsetColumnHeaderData> columnHeaders,
|
public static String validateToJdbcColumnName(String column, Map<String, ResultsetColumnHeaderData> headersByName, boolean allowEmpty) {
|
||||||
List<ApiParameterError> errors, boolean allowEmpty) {
|
ResultsetColumnHeaderData columnHeader = validateToJdbcColumn(column, headersByName, allowEmpty);
|
||||||
|
return columnHeader == null ? null : columnHeader.getColumnName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ResultsetColumnHeaderData validateToJdbcColumn(String column,
|
||||||
|
@NotNull Map<String, ResultsetColumnHeaderData> headersByName, boolean allowEmpty) {
|
||||||
|
final List<ApiParameterError> 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<String, ResultsetColumnHeaderData> headersByName, @NotNull List<ApiParameterError> errors, boolean allowEmpty) {
|
||||||
if (!allowEmpty && column == null) {
|
if (!allowEmpty && column == null) {
|
||||||
errors.add(parameterErrorWithValue("error.msg.column.empty", "Column filter is empty", API_PARAM_COLUMN, null));
|
errors.add(parameterErrorWithValue("error.msg.column.empty", "Column filter is empty", API_PARAM_COLUMN, null));
|
||||||
}
|
}
|
||||||
if (column != null) {
|
if (column == null) {
|
||||||
if (!columnHeaders.containsKey(column)) {
|
return null;
|
||||||
column = camelToSnake(column);
|
|
||||||
if (!columnHeaders.containsKey(column)) {
|
|
||||||
errors.add(
|
|
||||||
parameterErrorWithValue("error.msg.invalid.column", "Column not exist in database", API_PARAM_COLUMN, column));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return column;
|
Collection<ResultsetColumnHeaderData> 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<ColumnFilterData> columnFilters, @NotNull StringBuilder where,
|
public static boolean buildQueryCondition(List<ColumnFilterData> columnFilters, @NotNull StringBuilder where,
|
||||||
@NotNull List<Object> params, String alias, Map<String, ResultsetColumnHeaderData> columnHeaders, boolean embedded,
|
@NotNull List<Object> params, String alias, Map<String, ResultsetColumnHeaderData> headersByName, String dateFormat,
|
||||||
boolean strict, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
|
String dateTimeFormat, Locale locale, boolean embedded, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
|
||||||
if (columnFilters == null) {
|
if (columnFilters == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
boolean added = false;
|
boolean added = false;
|
||||||
int isize = columnFilters.size();
|
int isize = columnFilters.size();
|
||||||
for (int i = 0; i < isize; i++) {
|
for (int i = 0; i < isize; i++) {
|
||||||
boolean addedFilter = buildFilterCondition(columnFilters.get(i), where, params, alias, columnHeaders, embedded, strict,
|
boolean addedFilter = buildFilterCondition(columnFilters.get(i), where, params, alias, headersByName, dateFormat,
|
||||||
sqlGenerator);
|
dateTimeFormat, locale, embedded, sqlGenerator);
|
||||||
if (addedFilter && i < isize - 1) {
|
if (addedFilter && i < isize - 1) {
|
||||||
where.append(" AND ");
|
where.append(" AND ");
|
||||||
}
|
}
|
||||||
@@ -166,8 +194,8 @@ public final class SearchUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean buildFilterCondition(ColumnFilterData columnFilter, @NotNull StringBuilder where, @NotNull List<Object> params,
|
public static boolean buildFilterCondition(ColumnFilterData columnFilter, @NotNull StringBuilder where, @NotNull List<Object> params,
|
||||||
String alias, Map<String, ResultsetColumnHeaderData> columnHeaders, boolean embedded, boolean strict,
|
String alias, Map<String, ResultsetColumnHeaderData> headersByName, String dateFormat, String dateTimeFormat, Locale locale,
|
||||||
@NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
|
boolean embedded, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
|
||||||
String columnName = columnFilter.getColumn();
|
String columnName = columnFilter.getColumn();
|
||||||
List<FilterData> filters = columnFilter.getFilters();
|
List<FilterData> filters = columnFilter.getFilters();
|
||||||
int size = filters.size();
|
int size = filters.size();
|
||||||
@@ -175,26 +203,18 @@ public final class SearchUtil {
|
|||||||
if (!embedded && where.isEmpty()) {
|
if (!embedded && where.isEmpty()) {
|
||||||
where.append(" WHERE ");
|
where.append(" WHERE ");
|
||||||
}
|
}
|
||||||
|
ResultsetColumnHeaderData columnHeader = validateToJdbcColumn(columnName, headersByName, false);
|
||||||
|
|
||||||
FilterData filter = filters.get(i);
|
FilterData filter = filters.get(i);
|
||||||
SqlOperator operator = filter.getOperator();
|
SqlOperator operator = filter.getOperator();
|
||||||
List<String> values = filter.getValues();
|
List<String> values = filter.getValues();
|
||||||
List<Object> objectValues = values == null ? null
|
List<Object> 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();
|
.toList();
|
||||||
|
|
||||||
operator.validateValues(values);
|
buildCondition(columnHeader.getColumnName(), columnHeader.getColumnType(), operator, objectValues, where, params, alias,
|
||||||
if (operator.isPlaceholderSupported()) {
|
sqlGenerator);
|
||||||
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));
|
|
||||||
}
|
|
||||||
if (i < size - 1) {
|
if (i < size - 1) {
|
||||||
where.append(" AND ");
|
where.append(" AND ");
|
||||||
}
|
}
|
||||||
@@ -202,22 +222,24 @@ public final class SearchUtil {
|
|||||||
return size > 0;
|
return size > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Object parseAndValidateJdbcColumnValue(String column, String columnValue,
|
public static void buildCondition(@NotNull String definition, JdbcJavaType columnType, @NotNull SqlOperator operator,
|
||||||
Map<String, ResultsetColumnHeaderData> columnHeaders, boolean strict, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
|
List<Object> values, @NotNull StringBuilder where, @NotNull List<Object> params, String alias,
|
||||||
List<ApiParameterError> errors = new ArrayList<>();
|
@NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
|
||||||
column = validateToJdbcColumn(column, columnHeaders, errors, false);
|
int paramCount = values == null ? 0 : values.size();
|
||||||
if (!errors.isEmpty()) {
|
where.append(operator.formatPlaceholder(sqlGenerator, definition, paramCount, alias));
|
||||||
throw new PlatformApiDataValidationException(errors);
|
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,
|
public static Object parseJdbcColumnValue(@NotNull ResultsetColumnHeaderData columnHeader, String columnValue, String dateFormat,
|
||||||
final String dateFormat, final Locale locale, boolean strict, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
|
String dateTimeFormat, Locale locale, boolean strict, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
|
||||||
String columnValue = pValue;
|
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();
|
JdbcJavaType colType = columnHeader.getColumnType();
|
||||||
if (!colType.isStringType() || !columnHeader.isMandatory()) {
|
if (!colType.isStringType() || !columnHeader.isMandatory()) {
|
||||||
columnValue = StringUtils.trimToNull(columnValue);
|
columnValue = StringUtils.trimToNull(columnValue);
|
||||||
@@ -257,12 +279,14 @@ public final class SearchUtil {
|
|||||||
+ " - Invalid Type " + colType.getJdbcName(sqlGenerator.getDialect()) + " (neither varchar nor int)");
|
+ " - Invalid Type " + colType.getJdbcName(sqlGenerator.getDialect()) + " (neither varchar nor int)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
locale = locale == null ? ENGLISH : locale;
|
||||||
if (colType.isDateType()) {
|
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()) {
|
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()) {
|
if (colType.isAnyIntegerType()) {
|
||||||
return helper.convertToInteger(columnValue, columnHeader.getColumnName(), locale);
|
return helper.convertToInteger(columnValue, columnHeader.getColumnName(), locale);
|
||||||
|
|||||||
+7
@@ -23,6 +23,7 @@ import static org.apache.fineract.integrationtests.common.Utils.TENANT_PARAM_NAM
|
|||||||
|
|
||||||
import com.google.common.reflect.TypeToken;
|
import com.google.common.reflect.TypeToken;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
import io.restassured.path.json.JsonPath;
|
||||||
import io.restassured.specification.RequestSpecification;
|
import io.restassured.specification.RequestSpecification;
|
||||||
import io.restassured.specification.ResponseSpecification;
|
import io.restassured.specification.ResponseSpecification;
|
||||||
import jakarta.ws.rs.core.HttpHeaders;
|
import jakarta.ws.rs.core.HttpHeaders;
|
||||||
@@ -39,6 +40,7 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import org.apache.fineract.client.models.PagedLocalRequestAdvancedQueryRequest;
|
||||||
import org.apache.fineract.client.models.SavingsAccountTransactionsSearchResponse;
|
import org.apache.fineract.client.models.SavingsAccountTransactionsSearchResponse;
|
||||||
import org.apache.fineract.client.util.JSON;
|
import org.apache.fineract.client.util.JSON;
|
||||||
import org.apache.fineract.integrationtests.client.IntegrationTest;
|
import org.apache.fineract.integrationtests.client.IntegrationTest;
|
||||||
@@ -661,6 +663,11 @@ public class SavingsAccountHelper extends IntegrationTest {
|
|||||||
return GSON.fromJson(response, SavingsAccountTransactionsSearchResponse.class);
|
return GSON.fromJson(response, SavingsAccountTransactionsSearchResponse.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> querySavingsTransactions(Integer savingsId, PagedLocalRequestAdvancedQueryRequest request) {
|
||||||
|
String response = ok(fineract().savingsTransactions.advancedQuery1(savingsId.longValue(), request));
|
||||||
|
return JsonPath.from(response).get("");
|
||||||
|
}
|
||||||
|
|
||||||
public List<HashMap> getSavingsTransactions(final Integer savingsID) {
|
public List<HashMap> getSavingsTransactions(final Integer savingsID) {
|
||||||
final Object get = getSavingsCollectionAttribute(savingsID, "transactions");
|
final Object get = getSavingsCollectionAttribute(savingsID, "transactions");
|
||||||
final String json = new Gson().toJson(get);
|
final String json = new Gson().toJson(get);
|
||||||
|
|||||||
+7
@@ -24,6 +24,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
|||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
|
import io.restassured.path.json.JsonPath;
|
||||||
import io.restassured.specification.RequestSpecification;
|
import io.restassured.specification.RequestSpecification;
|
||||||
import io.restassured.specification.ResponseSpecification;
|
import io.restassured.specification.ResponseSpecification;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -31,8 +32,10 @@ import java.util.Calendar;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import org.apache.fineract.client.models.GetDataTablesResponse;
|
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.PostDataTablesAppTableIdResponse;
|
||||||
import org.apache.fineract.client.models.PostDataTablesRequest;
|
import org.apache.fineract.client.models.PostDataTablesRequest;
|
||||||
import org.apache.fineract.client.models.PostDataTablesResponse;
|
import org.apache.fineract.client.models.PostDataTablesResponse;
|
||||||
@@ -294,4 +297,8 @@ public class DatatableHelper extends IntegrationTest {
|
|||||||
return GSON.fromJson(response, PutDataTablesResponse.class);
|
return GSON.fromJson(response, PutDataTablesResponse.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> queryDatatable(String dataTableName, PagedLocalRequestAdvancedQueryData request) {
|
||||||
|
String response = ok(fineract().dataTables.advancedQuery(dataTableName, request));
|
||||||
|
return JsonPath.from(response).get("");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+387
@@ -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<String, Object> response = datatableHelper.queryDatatable(datatable, pagedQuery);
|
||||||
|
|
||||||
|
assertEquals(1, response.get("total"));
|
||||||
|
List content = (List) response.get("content");
|
||||||
|
assertNotNull(content);
|
||||||
|
assertEquals(1, content.size());
|
||||||
|
Map<String, Object> first = (Map<String, Object>) 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<String, Object> response = savingsAccountHelper.querySavingsTransactions(savingsId, pagedRequest);
|
||||||
|
|
||||||
|
assertEquals(1, response.get("total"));
|
||||||
|
List content = (List) response.get("content");
|
||||||
|
assertNotNull(content);
|
||||||
|
assertEquals(1, content.size());
|
||||||
|
Map<String, Object> first = (Map<String, Object>) 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<String, Object> 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<HashMap<String, Object>> 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<ResultsetColumnHeaderData> columnHeaderData = dataTable.getColumnHeaderData();
|
||||||
|
assertNotNull(columnHeaderData);
|
||||||
|
// pk column and 2 audit columns were added automatically
|
||||||
|
assertEquals(9, columnHeaderData.size());
|
||||||
|
return datatable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private HashMap<String, Object> createDatatableEntry(String datatable, Integer apptableId, LocalDate dateValue, Boolean boolValue,
|
||||||
|
Integer intValue, BigDecimal decValue) {
|
||||||
|
final HashMap<String, Object> 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<String, Object> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+65
-62
@@ -456,83 +456,86 @@ public class DatatableIntegrationTest extends IntegrationTest {
|
|||||||
// creating new client datatable entry
|
// creating new client datatable entry
|
||||||
final boolean genericResultSet = true;
|
final boolean genericResultSet = true;
|
||||||
|
|
||||||
HashMap<String, Object> datatableEntryMap = new HashMap<>();
|
HashMap<String, Object> firstEntryMap = new HashMap<>();
|
||||||
datatableEntryMap.put("itsABoolean", null);
|
firstEntryMap.put("itsABoolean", null);
|
||||||
datatableEntryMap.put("itsADate", null);
|
firstEntryMap.put("itsADate", null);
|
||||||
datatableEntryMap.put("itsADatetime", null);
|
firstEntryMap.put("itsADatetime", null);
|
||||||
datatableEntryMap.put("itsADecimal", null);
|
firstEntryMap.put("itsADecimal", null);
|
||||||
datatableEntryMap.put("TST_TST_TST_cd_itsADropdown", null);
|
firstEntryMap.put("TST_TST_TST_cd_itsADropdown", null);
|
||||||
datatableEntryMap.put("itsANumber", null);
|
firstEntryMap.put("itsANumber", null);
|
||||||
datatableEntryMap.put("itsAString", null);
|
firstEntryMap.put("itsAString", null);
|
||||||
datatableEntryMap.put("itsAText", null);
|
firstEntryMap.put("itsAText", null);
|
||||||
|
|
||||||
datatableEntryMap.put("locale", "en");
|
firstEntryMap.put("locale", "en");
|
||||||
datatableEntryMap.put("dateFormat", "yyyy-MM-dd");
|
firstEntryMap.put("dateFormat", "yyyy-MM-dd");
|
||||||
|
|
||||||
String datatableEntryRequestJsonString = new GsonBuilder().serializeNulls().create().toJson(datatableEntryMap);
|
String firstEntryRequestJsonString = new GsonBuilder().serializeNulls().create().toJson(firstEntryMap);
|
||||||
LOG.info("map : {}", datatableEntryRequestJsonString);
|
LOG.info("map : {}", firstEntryRequestJsonString);
|
||||||
|
|
||||||
HashMap<String, Object> datatableEntryResponseFirst = this.datatableHelper.createDatatableEntry(datatableName, loanID,
|
HashMap<String, Object> firstEntryResponse = this.datatableHelper.createDatatableEntry(datatableName, loanID, genericResultSet,
|
||||||
genericResultSet, datatableEntryRequestJsonString);
|
firstEntryRequestJsonString);
|
||||||
|
assertNotNull(firstEntryResponse.get("resourceId"), "ERROR IN CREATING THE ENTITY DATATABLE RECORD");
|
||||||
|
|
||||||
datatableEntryMap = new HashMap<>();
|
HashMap<String, Object> secondEntryMap = new HashMap<>();
|
||||||
datatableEntryMap.put("itsABoolean", "");
|
secondEntryMap.put("itsABoolean", "");
|
||||||
datatableEntryMap.put("itsADate", "");
|
secondEntryMap.put("itsADate", "");
|
||||||
datatableEntryMap.put("itsADatetime", "");
|
secondEntryMap.put("itsADatetime", "");
|
||||||
datatableEntryMap.put("itsADecimal", "");
|
secondEntryMap.put("itsADecimal", "");
|
||||||
datatableEntryMap.put("TST_TST_TST_cd_itsADropdown", "");
|
secondEntryMap.put("TST_TST_TST_cd_itsADropdown", "");
|
||||||
datatableEntryMap.put("itsANumber", "");
|
secondEntryMap.put("itsANumber", "");
|
||||||
datatableEntryMap.put("itsAString", "");
|
secondEntryMap.put("itsAString", "");
|
||||||
datatableEntryMap.put("itsAText", "");
|
secondEntryMap.put("itsAText", "");
|
||||||
|
|
||||||
datatableEntryMap.put("locale", "en");
|
secondEntryMap.put("locale", "en");
|
||||||
datatableEntryMap.put("dateFormat", "yyyy-MM-dd");
|
secondEntryMap.put("dateFormat", "yyyy-MM-dd");
|
||||||
|
|
||||||
datatableEntryRequestJsonString = new GsonBuilder().serializeNulls().create().toJson(datatableEntryMap);
|
String secondEntryRequestJsonString = new GsonBuilder().serializeNulls().create().toJson(secondEntryMap);
|
||||||
HashMap<String, Object> datatableEntryResponseSecond = this.datatableHelper.createDatatableEntry(datatableName, loanID,
|
HashMap<String, Object> secondEntryResponse = this.datatableHelper.createDatatableEntry(datatableName, loanID, genericResultSet,
|
||||||
genericResultSet, datatableEntryRequestJsonString);
|
secondEntryRequestJsonString);
|
||||||
assertNotNull(datatableEntryResponseFirst.get("resourceId"), "ERROR IN CREATING THE ENTITY DATATABLE RECORD");
|
assertNotNull(secondEntryResponse.get("resourceId"), "ERROR IN CREATING THE ENTITY DATATABLE RECORD");
|
||||||
assertNotNull(datatableEntryResponseSecond.get("resourceId"), "ERROR IN CREATING THE ENTITY DATATABLE RECORD");
|
|
||||||
|
|
||||||
// Read the Datatable entry generated with genericResultSet in true (default)
|
// Read the Datatable entry generated with genericResultSet in true (default)
|
||||||
HashMap<String, Object> items = this.datatableHelper.readDatatableEntry(datatableName, loanID, genericResultSet, null, "");
|
HashMap<String, Object> items = this.datatableHelper.readDatatableEntry(datatableName, loanID, genericResultSet, null, "");
|
||||||
assertNotNull(items);
|
assertNotNull(items);
|
||||||
assertEquals(2, ((List) items.get("data")).size());
|
assertEquals(2, ((List) items.get("data")).size());
|
||||||
|
|
||||||
assertEquals("id", ((Map) ((List) items.get("columnHeaders")).get(0)).get("columnName"));
|
List headers = (List) items.get("columnHeaders");
|
||||||
assertEquals(1, ((List) ((Map) ((List) items.get("data")).get(0)).get("row")).get(0));
|
List firstEntryValues = (List) ((Map) ((List) items.get("data")).get(0)).get("row");
|
||||||
assertEquals("loan_id", ((Map) ((List) items.get("columnHeaders")).get(1)).get("columnName"));
|
assertEquals("id", ((Map) headers.get(0)).get("columnName"));
|
||||||
assertEquals(loanID, ((List) ((Map) ((List) items.get("data")).get(0)).get("row")).get(1));
|
assertEquals(1, firstEntryValues.get(0));
|
||||||
assertEquals("itsABoolean", ((Map) ((List) items.get("columnHeaders")).get(2)).get("columnName"));
|
assertEquals("loan_id", ((Map) headers.get(1)).get("columnName"));
|
||||||
assertNull(((List) ((Map) ((List) items.get("data")).get(0)).get("row")).get(2));
|
assertEquals(loanID, firstEntryValues.get(1));
|
||||||
assertEquals("itsADate", ((Map) ((List) items.get("columnHeaders")).get(3)).get("columnName"));
|
assertEquals("itsABoolean", ((Map) headers.get(2)).get("columnName"));
|
||||||
assertNull(((List) ((Map) ((List) items.get("data")).get(0)).get("row")).get(3));
|
assertNull(firstEntryValues.get(2));
|
||||||
assertEquals("itsADatetime", ((Map) ((List) items.get("columnHeaders")).get(4)).get("columnName"));
|
assertEquals("itsADate", ((Map) headers.get(3)).get("columnName"));
|
||||||
assertNull(((List) ((Map) ((List) items.get("data")).get(0)).get("row")).get(4));
|
assertNull(firstEntryValues.get(3));
|
||||||
assertEquals("itsADecimal", ((Map) ((List) items.get("columnHeaders")).get(5)).get("columnName"));
|
assertEquals("itsADatetime", ((Map) headers.get(4)).get("columnName"));
|
||||||
assertNull(((List) ((Map) ((List) items.get("data")).get(0)).get("row")).get(5));
|
assertNull(firstEntryValues.get(4));
|
||||||
assertEquals("TST_TST_TST_cd_itsADropdown", ((Map) ((List) items.get("columnHeaders")).get(6)).get("columnName"));
|
assertEquals("itsADecimal", ((Map) headers.get(5)).get("columnName"));
|
||||||
assertNull(((List) ((Map) ((List) items.get("data")).get(0)).get("row")).get(6));
|
assertNull(firstEntryValues.get(5));
|
||||||
assertEquals("itsANumber", ((Map) ((List) items.get("columnHeaders")).get(7)).get("columnName"));
|
assertEquals("TST_TST_TST_cd_itsADropdown", ((Map) headers.get(6)).get("columnName"));
|
||||||
assertNull(((List) ((Map) ((List) items.get("data")).get(0)).get("row")).get(7));
|
assertNull(firstEntryValues.get(6));
|
||||||
assertEquals("itsAString", ((Map) ((List) items.get("columnHeaders")).get(8)).get("columnName"));
|
assertEquals("itsANumber", ((Map) headers.get(7)).get("columnName"));
|
||||||
assertNull(((List) ((Map) ((List) items.get("data")).get(0)).get("row")).get(8));
|
assertNull(firstEntryValues.get(7));
|
||||||
assertEquals("itsAText", ((Map) ((List) items.get("columnHeaders")).get(9)).get("columnName"));
|
assertEquals("itsAString", ((Map) headers.get(8)).get("columnName"));
|
||||||
assertNull(((List) ((Map) ((List) items.get("data")).get(0)).get("row")).get(9));
|
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));
|
List secondEntryValues = (List) ((Map) ((List) items.get("data")).get(1)).get("row");
|
||||||
assertEquals(loanID, ((List) ((Map) ((List) items.get("data")).get(1)).get("row")).get(1));
|
assertEquals(2, secondEntryValues.get(0));
|
||||||
assertNull(((List) ((Map) ((List) items.get("data")).get(1)).get("row")).get(2));
|
assertEquals(loanID, secondEntryValues.get(1));
|
||||||
assertNull(((List) ((Map) ((List) items.get("data")).get(1)).get("row")).get(3));
|
assertNull(secondEntryValues.get(2));
|
||||||
assertNull(((List) ((Map) ((List) items.get("data")).get(1)).get("row")).get(4));
|
assertNull(secondEntryValues.get(3));
|
||||||
assertNull(((List) ((Map) ((List) items.get("data")).get(1)).get("row")).get(5));
|
assertNull(secondEntryValues.get(4));
|
||||||
assertNull(((List) ((Map) ((List) items.get("data")).get(1)).get("row")).get(6));
|
assertNull(secondEntryValues.get(5));
|
||||||
assertNull(((List) ((Map) ((List) items.get("data")).get(1)).get("row")).get(7));
|
assertNull(secondEntryValues.get(6));
|
||||||
assertNull(((List) ((Map) ((List) items.get("data")).get(1)).get("row")).get(8));
|
assertNull(secondEntryValues.get(7));
|
||||||
assertNull(((List) ((Map) ((List) items.get("data")).get(1)).get("row")).get(9));
|
assertNull(secondEntryValues.get(8));
|
||||||
|
assertNull(secondEntryValues.get(9));
|
||||||
|
|
||||||
PutDataTablesAppTableIdDatatableIdResponse updatedDatatableEntryResponse = this.datatableHelper.updateDatatableEntry(datatableName,
|
PutDataTablesAppTableIdDatatableIdResponse updatedDatatableEntryResponse = this.datatableHelper.updateDatatableEntry(datatableName,
|
||||||
loanID, 1, datatableEntryRequestJsonString);
|
loanID, 1, secondEntryRequestJsonString);
|
||||||
assertNotNull(updatedDatatableEntryResponse);
|
assertNotNull(updatedDatatableEntryResponse);
|
||||||
assertEquals(0, updatedDatatableEntryResponse.getChanges().size());
|
assertEquals(0, updatedDatatableEntryResponse.getChanges().size());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user