FINERACT-2076: SQL query optimization

This commit is contained in:
Aleksandar Vidakovic
2024-04-25 14:08:04 +02:00
committed by Aleksandar Vidakovic
parent 039bca440b
commit a022a51189
85 changed files with 1186 additions and 1193 deletions
@@ -76,6 +76,8 @@ public class FineractProperties {
private FineractModulesProperties module;
private FineractSqlValidationProperties sqlValidation;
@Getter
@Setter
public static class FineractTenantProperties {
@@ -533,4 +535,38 @@ public class FineractProperties {
public static class FineractInvestorModuleProperties extends AbstractFineractModuleProperties {
}
@Getter
@Setter
public static class FineractSqlValidationProperties {
private List<FineractSqlValidationPatternProperties> patterns;
private List<FineractSqlValidationProfileProperties> profiles;
}
@Getter
@Setter
public static class FineractSqlValidationProfileProperties {
private String name;
private String description;
private List<FineractSqlValidationPatternReferenceProperties> patternRefs;
private Boolean enabled = true;
}
@Getter
@Setter
public static class FineractSqlValidationPatternReferenceProperties {
private String name;
private Integer order;
}
@Getter
@Setter
public static class FineractSqlValidationPatternProperties {
private String name;
private String pattern;
}
}
@@ -18,101 +18,62 @@
*/
package org.apache.fineract.infrastructure.core.data;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.security.utils.SQLInjectionValidator;
/**
* <p>
* Immutable data object representing pagination parameter values.
* </p>
*/
public final class PaginationParameters {
@Builder
@Getter
public class PaginationParameters {
private final boolean paged;
private final Integer offset;
private final Integer limit;
private final String orderBy;
private final String sortOrder;
// TODO: why do we really need this class? SearchParameters seems to provide similar functionality
public static PaginationParameters instance(Boolean paged, Integer offset, Integer limit, String orderBy, String sortOrder) {
if (null == paged) {
paged = false;
}
public static final int DEFAULT_MAX_LIMIT = 200;
final Integer maxLimitAllowed = getCheckedLimit(limit);
return new PaginationParameters(paged, offset, maxLimitAllowed, orderBy, sortOrder);
}
private PaginationParameters(boolean paged, Integer offset, Integer limit, String orderBy, String sortOrder) {
SQLInjectionValidator.validateSQLInput(orderBy);
SQLInjectionValidator.validateSQLInput(sortOrder);
this.paged = paged;
this.offset = offset;
this.limit = limit;
this.orderBy = orderBy;
this.sortOrder = sortOrder;
}
public static Integer getCheckedLimit(final Integer limit) {
final Integer maxLimitAllowed = 200;
// default to max limit first off
Integer checkedLimit = maxLimitAllowed;
if (limit != null && limit > 0) {
checkedLimit = limit;
} else if (limit != null) {
// unlimited case: limit provided and 0 or less
checkedLimit = null;
}
return checkedLimit;
}
public boolean isPaged() {
return this.paged;
}
public Integer getOffset() {
return this.offset;
}
private boolean paged;
private Integer offset;
@Getter(AccessLevel.NONE)
private Integer limit;
private String orderBy;
private String sortOrder;
public Integer getLimit() {
return this.limit;
if (limit == null) {
return DEFAULT_MAX_LIMIT;
}
if (limit > 0) {
return limit;
}
return null; // unlimited (0 or less)
}
public String getOrderBy() {
return this.orderBy;
}
public String getSortOrder() {
return this.sortOrder;
}
public boolean isOrderByRequested() {
public boolean hasOrderBy() {
return StringUtils.isNotBlank(this.orderBy);
}
public boolean isSortOrderProvided() {
public boolean hasSortOrder() {
return StringUtils.isNotBlank(this.sortOrder);
}
public boolean isLimited() {
return this.limit != null && this.limit.intValue() > 0;
public boolean hasLimit() {
return this.limit != null && this.limit > 0;
}
public boolean isOffset() {
public boolean hasOffset() {
return this.offset != null;
}
// TODO: following functions are just doing too much in one place; will disappear with type safe queries
public String orderBySql() {
final StringBuilder sql = new StringBuilder();
if (this.isOrderByRequested()) {
if (this.hasOrderBy()) {
sql.append(" order by ").append(this.getOrderBy());
if (this.isSortOrderProvided()) {
if (this.hasSortOrder()) {
sql.append(' ').append(this.getSortOrder());
}
}
@@ -121,9 +82,9 @@ public final class PaginationParameters {
public String limitSql() {
final StringBuilder sql = new StringBuilder();
if (this.isLimited()) {
if (this.hasLimit()) {
sql.append(" limit ").append(this.getLimit());
if (this.isOffset()) {
if (this.hasOffset()) {
sql.append(" offset ").append(this.getOffset());
}
}
@@ -132,10 +93,10 @@ public final class PaginationParameters {
public String paginationSql() {
final StringBuilder sqlBuilder = new StringBuilder(50);
if (this.isOrderByRequested()) {
if (this.hasOrderBy()) {
sqlBuilder.append(' ').append(this.orderBySql());
}
if (this.isLimited()) {
if (this.hasLimit()) {
sqlBuilder.append(' ').append(this.limitSql());
}
@@ -34,7 +34,7 @@ public class PaginationParametersDataValidator {
public void validateParameterValues(PaginationParameters parameters, final Set<String> supportedOrdeByValues,
final String resourceName) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
if (parameters.isOrderByRequested() && !supportedOrdeByValues.contains(parameters.getOrderBy())) {
if (parameters.hasOrderBy() && !supportedOrdeByValues.contains(parameters.getOrderBy())) {
final String defaultUserMessage = "The orderBy value '" + parameters.getOrderBy()
+ "' is not supported. The supported orderBy values are " + supportedOrdeByValues;
final ApiParameterError error = ApiParameterError.parameterError(
@@ -43,7 +43,7 @@ public class PaginationParametersDataValidator {
dataValidationErrors.add(error);
}
if (parameters.isSortOrderProvided() && !sortOrderValues.contains(parameters.getSortOrder().toUpperCase())) {
if (parameters.hasSortOrder() && !sortOrderValues.contains(parameters.getSortOrder().toUpperCase())) {
final String defaultUserMessage = "The sortOrder value '" + parameters.getSortOrder()
+ "' is not supported. The supported sortOrder values are " + sortOrderValues;
final ApiParameterError error = ApiParameterError.parameterError(
@@ -18,518 +18,113 @@
*/
package org.apache.fineract.infrastructure.core.service;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
@Builder
@Getter
@AllArgsConstructor
public final class SearchParameters {
public class SearchParameters {
private final Long officeId;
private final String externalId;
private final String name;
private final String hierarchy;
private final String firstname;
private final String lastname;
private final String status;
private final Integer offset;
private final Integer limit;
private final String orderBy;
private final String sortOrder;
private final String accountNo;
private final String currencyCode;
public static final int DEFAULT_MAX_LIMIT = 200;
private final Long staffId;
private Long officeId;
private String externalId;
private String name;
private String hierarchy;
private String firstname;
private String lastname;
private String status;
private Integer offset;
@Getter(AccessLevel.NONE)
private Integer limit;
private String orderBy;
private String sortOrder;
private String accountNo;
private String currencyCode;
private Long staffId;
private Long loanId;
private Long savingsId;
@Getter(AccessLevel.NONE)
private Boolean orphansOnly;
private Long provisioningEntryId;
private Long productId;
private Long categoryId;
@Getter(AccessLevel.NONE)
private Boolean isSelfUser;
private final Long loanId;
public Integer getLimit() {
if (limit == null) {
return DEFAULT_MAX_LIMIT;
}
private final Long savingsId;
private final Boolean orphansOnly;
if (limit > 0) {
return limit;
}
// Provisioning Entries Search Params
private final Long provisioningEntryId;
private final Long productId;
private final Long categoryId;
private final boolean isSelfUser;
public static SearchParameters from(final Long officeId, final String externalId, final String name, final String hierarchy) {
final Long staffId = null;
final String accountNo = null;
final Long loanId = null;
final Long savingsId = null;
final Boolean orphansOnly = false;
final boolean isSelfUser = false;
return new SearchParameters(officeId, externalId, name, hierarchy, null, null, null, null, null, null, staffId, accountNo, loanId,
savingsId, orphansOnly, isSelfUser);
return null; // unlimited (0 or less)
}
public static SearchParameters forClients(final Long officeId, final String externalId, final String displayName,
final String firstname, final String lastname, final String status, final String hierarchy, final Integer offset,
final Integer limit, final String orderBy, final String sortOrder, final Boolean orphansOnly, final boolean isSelfUser) {
final Integer maxLimitAllowed = getCheckedLimit(limit);
final Long staffId = null;
final String accountNo = null;
final Long loanId = null;
final Long savingsId = null;
return new SearchParameters(officeId, externalId, displayName, hierarchy, firstname, lastname, status, offset, maxLimitAllowed,
orderBy, sortOrder, staffId, accountNo, loanId, savingsId, orphansOnly, isSelfUser);
public Boolean getOrphansOnly() {
return Boolean.TRUE.equals(orphansOnly);
}
public static SearchParameters forGroups(final Long officeId, final Long staffId, final String externalId, final String name,
final String hierarchy, final Integer offset, final Integer limit, final String orderBy, final String sortOrder,
final Boolean orphansOnly) {
final Integer maxLimitAllowed = getCheckedLimit(limit);
final String accountNo = null;
final Long loanId = null;
final Long savingsId = null;
final boolean isSelfUser = false;
return new SearchParameters(officeId, externalId, name, hierarchy, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId,
accountNo, loanId, savingsId, orphansOnly, isSelfUser);
public Boolean getIsSelfUser() {
return Boolean.TRUE.equals(isSelfUser);
}
public static SearchParameters forOffices(final String orderBy, final String sortOrder) {
final Boolean orphansOnly = false;
final boolean isSelfUser = false;
return new SearchParameters(null, null, null, null, null, null, null, null, orderBy, sortOrder, null, null, null, null, orphansOnly,
isSelfUser);
}
public static SearchParameters forLoans(final String externalId, final Integer offset, final Integer limit, final String orderBy,
final String sortOrder, final String accountNo) {
final Integer maxLimitAllowed = getCheckedLimit(limit);
final Long staffId = null;
final Long loanId = null;
final Long savingsId = null;
final Boolean orphansOnly = false;
final boolean isSelfUser = false;
return new SearchParameters(null, externalId, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId,
accountNo, loanId, savingsId, orphansOnly, isSelfUser);
}
public static SearchParameters forJournalEntries(final Long officeId, final Integer offset, final Integer limit, final String orderBy,
final String sortOrder, final Long loanId, final Long savingsId) {
final Integer maxLimitAllowed = getCheckedLimit(limit);
final Long staffId = null;
final Boolean orphansOnly = false;
final boolean isSelfUser = false;
return new SearchParameters(officeId, null, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId, null,
loanId, savingsId, orphansOnly, isSelfUser);
}
public static SearchParameters forJournalEntries(final Long officeId, final Integer offset, final Integer limit, final String orderBy,
final String sortOrder, final Long loanId, final Long savingsId, final String currencyCode) {
final Integer maxLimitAllowed = getCheckedLimit(limit);
final Long staffId = null;
final Boolean orphansOnly = false;
return new SearchParameters(officeId, null, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId, null,
loanId, savingsId, orphansOnly, currencyCode);
}
public static SearchParameters forPagination(final Integer offset, final Integer limit, final String orderBy, final String sortOrder) {
final Integer maxLimitAllowed = getCheckedLimit(limit);
final Long staffId = null;
final Long loanId = null;
final Long savingsId = null;
final Boolean orphansOnly = false;
final boolean isSelfUser = false;
return new SearchParameters(null, null, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId, null, loanId,
savingsId, orphansOnly, isSelfUser);
}
public static SearchParameters forPaginationAndAccountNumberSearch(final Integer offset, final Integer limit, final String orderBy,
final String sortOrder, final String accountNumber) {
final Integer maxLimitAllowed = getCheckedLimit(limit);
final Long staffId = null;
final Long loanId = null;
final Long savingsId = null;
final Boolean orphansOnly = false;
final boolean isSelfUser = false;
return new SearchParameters(null, null, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId, accountNumber,
loanId, savingsId, orphansOnly, isSelfUser);
}
public static SearchParameters forPagination(final Integer offset, final Integer limit) {
final Integer maxLimitAllowed = getCheckedLimit(limit);
final Long staffId = null;
final Long loanId = null;
final Long savingsId = null;
final Boolean orphansOnly = false;
final String orderBy = null;
final String sortOrder = null;
final boolean isSelfUser = false;
return new SearchParameters(null, null, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId, null, loanId,
savingsId, orphansOnly, isSelfUser);
}
public static SearchParameters forProvisioningEntries(final Long provisioningEntryId, final Long officeId, final Long productId,
final Long categoryId, final Integer offset, final Integer limit) {
return new SearchParameters(provisioningEntryId, officeId, productId, categoryId, offset, limit);
}
public static SearchParameters forSavings(final String externalId, final Integer offset, final Integer limit, final String orderBy,
final String sortOrder) {
final Integer maxLimitAllowed = getCheckedLimit(limit);
final Long staffId = null;
final String accountNo = null;
final Long loanId = null;
final Long savingsId = null;
final Boolean orphansOnly = false;
final boolean isSelfUser = false;
return new SearchParameters(null, externalId, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId,
accountNo, loanId, savingsId, orphansOnly, isSelfUser);
}
public static SearchParameters forAccountTransfer(final String externalId, final Integer offset, final Integer limit,
final String orderBy, final String sortOrder) {
final Integer maxLimitAllowed = getCheckedLimit(limit);
final Long staffId = null;
final String accountNo = null;
final Long loanId = null;
final Long savingsId = null;
final Boolean orphansOnly = false;
final boolean isSelfUser = false;
return new SearchParameters(null, externalId, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId,
accountNo, loanId, savingsId, orphansOnly, isSelfUser);
}
public static SearchParameters forSMSCampaign(final Integer offset, final Integer limit, final String orderBy, final String sortOrder) {
final String externalId = null;
final Integer maxLimitAllowed = getCheckedLimit(limit);
final Long staffId = null;
final String accountNo = null;
final Long loanId = null;
final Long savingsId = null;
final Boolean orphansOnly = false;
final boolean isSelfUser = false;
return new SearchParameters(null, externalId, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId,
accountNo, loanId, savingsId, orphansOnly, isSelfUser);
}
public static SearchParameters forEmailCampaign(final Integer offset, final Integer limit, final String orderBy,
final String sortOrder) {
final String externalId = null;
final Integer maxLimitAllowed = getCheckedLimit(limit);
final Long staffId = null;
final String accountNo = null;
final Long loanId = null;
final Long savingsId = null;
final Boolean orphansOnly = false;
final boolean isSelfUser = false;
return new SearchParameters(null, externalId, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId,
accountNo, loanId, savingsId, orphansOnly, isSelfUser);
}
private SearchParameters(final Long officeId, final String externalId, final String name, final String hierarchy,
final String firstname, final String lastname, final String status, final Integer offset, final Integer limit,
final String orderBy, final String sortOrder, final Long staffId, final String accountNo, final Long loanId,
final Long savingsId, final Boolean orphansOnly, boolean isSelfUser) {
this.officeId = officeId;
this.externalId = externalId;
this.name = name;
this.hierarchy = hierarchy;
this.firstname = firstname;
this.lastname = lastname;
this.offset = offset;
this.limit = limit;
this.orderBy = orderBy;
this.sortOrder = sortOrder;
this.staffId = staffId;
this.accountNo = accountNo;
this.loanId = loanId;
this.savingsId = savingsId;
this.orphansOnly = orphansOnly;
this.currencyCode = null;
this.provisioningEntryId = null;
this.productId = null;
this.categoryId = null;
this.isSelfUser = isSelfUser;
this.status = status;
}
private SearchParameters(final Long officeId, final String externalId, final String name, final String hierarchy,
final String firstname, final String lastname, final Integer offset, final Integer limit, final String orderBy,
final String sortOrder, final Long staffId, final String accountNo, final Long loanId, final Long savingsId,
final Boolean orphansOnly, boolean isSelfUser) {
this.officeId = officeId;
this.externalId = externalId;
this.name = name;
this.hierarchy = hierarchy;
this.firstname = firstname;
this.lastname = lastname;
this.offset = offset;
this.limit = limit;
this.orderBy = orderBy;
this.sortOrder = sortOrder;
this.staffId = staffId;
this.accountNo = accountNo;
this.loanId = loanId;
this.savingsId = savingsId;
this.orphansOnly = orphansOnly;
this.currencyCode = null;
this.provisioningEntryId = null;
this.productId = null;
this.categoryId = null;
this.isSelfUser = isSelfUser;
this.status = null;
}
private SearchParameters(final Long provisioningEntryId, final Long officeId, final Long productId, final Long categoryId,
final Integer offset, final Integer limit) {
this.externalId = null;
this.name = null;
this.hierarchy = null;
this.firstname = null;
this.lastname = null;
this.orderBy = null;
this.sortOrder = null;
this.staffId = null;
this.accountNo = null;
this.loanId = null;
this.savingsId = null;
this.orphansOnly = null;
this.currencyCode = null;
this.officeId = officeId;
this.offset = offset;
this.limit = limit;
this.provisioningEntryId = provisioningEntryId;
this.productId = productId;
this.categoryId = categoryId;
this.isSelfUser = false;
this.status = null;
}
public SearchParameters(final Long officeId, final String externalId, final String name, final String hierarchy, final String firstname,
final String lastname, final Integer offset, final Integer limit, final String orderBy, final String sortOrder,
final Long staffId, final String accountNo, final Long loanId, final Long savingsId, final Boolean orphansOnly,
final String currencyCode) {
this.officeId = officeId;
this.externalId = externalId;
this.name = name;
this.hierarchy = hierarchy;
this.firstname = firstname;
this.lastname = lastname;
this.offset = offset;
this.limit = limit;
this.orderBy = orderBy;
this.sortOrder = sortOrder;
this.staffId = staffId;
this.accountNo = accountNo;
this.loanId = loanId;
this.savingsId = savingsId;
this.orphansOnly = orphansOnly;
this.currencyCode = currencyCode;
this.provisioningEntryId = null;
this.productId = null;
this.categoryId = null;
this.isSelfUser = false;
this.status = null;
}
public boolean isOrderByRequested() {
public boolean hasOrderBy() {
return StringUtils.isNotBlank(this.orderBy);
}
public boolean isSortOrderProvided() {
public boolean hasSortOrder() {
return StringUtils.isNotBlank(this.sortOrder);
}
public static Integer getCheckedLimit(final Integer limit) {
final Integer maxLimitAllowed = 200;
// default to max limit first off
Integer checkedLimit = maxLimitAllowed;
if (limit != null && limit > 0) {
checkedLimit = limit;
} else if (limit != null) {
// unlimited case: limit provided and 0 or less
checkedLimit = null;
}
return checkedLimit;
}
public boolean isOfficeIdPassed() {
public boolean hasOfficeId() {
return this.officeId != null && this.officeId != 0;
}
public boolean isCurrencyCodePassed() {
return this.currencyCode != null;
public boolean hasCurrencyCode() {
return StringUtils.isNotBlank(this.currencyCode);
}
public boolean isLimited() {
return this.limit != null && this.limit.intValue() > 0;
public boolean hasLimit() {
return this.limit != null && this.limit > 0;
}
public boolean isOffset() {
public boolean hasOffset() {
return this.offset != null;
}
public boolean isScopedByOfficeHierarchy() {
public boolean hasHierarchy() {
return StringUtils.isNotBlank(this.hierarchy);
}
public Long getOfficeId() {
return this.officeId;
}
public String getCurrencyCode() {
return this.currencyCode;
}
public String getExternalId() {
return this.externalId;
}
public String getName() {
return this.name;
}
public String getHierarchy() {
return this.hierarchy;
}
public String getFirstname() {
return this.firstname;
}
public String getLastname() {
return this.lastname;
}
public String getStatus() {
return this.status;
}
public Integer getOffset() {
return this.offset;
}
public Integer getLimit() {
return this.limit;
}
public String getOrderBy() {
return this.orderBy;
}
public String getSortOrder() {
return this.sortOrder;
}
public boolean isStaffIdPassed() {
public boolean hasStaffId() {
return this.staffId != null && this.staffId != 0;
}
public Long getStaffId() {
return this.staffId;
}
public String getAccountNo() {
return this.accountNo;
}
public boolean isLoanIdPassed() {
public boolean hasLoanId() {
return this.loanId != null && this.loanId != 0;
}
public boolean isSavingsIdPassed() {
public boolean hasSavingsId() {
return this.savingsId != null && this.savingsId != 0;
}
public Long getLoanId() {
return this.loanId;
}
public Long getSavingsId() {
return this.savingsId;
}
public Boolean isOrphansOnly() {
if (this.orphansOnly != null) {
return this.orphansOnly;
}
return false;
}
public Long getProvisioningEntryId() {
return this.provisioningEntryId;
}
public boolean isProvisioningEntryIdPassed() {
public boolean hasProvisioningEntryId() {
return this.provisioningEntryId != null && this.provisioningEntryId != 0;
}
public Long getProductId() {
return this.productId;
}
public boolean isProductIdPassed() {
public boolean hasProductId() {
return this.productId != null && this.productId != 0;
}
public Long getCategoryId() {
return this.categoryId;
}
public boolean isCategoryIdPassed() {
public boolean hasCategoryId() {
return this.categoryId != null && this.categoryId != 0;
}
public boolean isSelfUser() {
return this.isSelfUser;
}
/**
* creates an instance of the SearchParameters from a request for the report mailing job run history
*
* @return SearchParameters object
**/
public static SearchParameters fromReportMailingJobRunHistory(final Integer offset, final Integer limit, final String orderBy,
final String sortOrder) {
final Integer maxLimitAllowed = getCheckedLimit(limit);
return new SearchParameters(null, null, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, null, null, null, null,
null, false);
}
/**
* creates an instance of the {@link SearchParameters} from a request for the report mailing job
*
* @return {@link SearchParameters} object
*/
public static SearchParameters fromReportMailingJob(final Integer offset, final Integer limit, final String orderBy,
final String sortOrder) {
final Integer maxLimitAllowed = getCheckedLimit(limit);
return new SearchParameters(null, null, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, null, null, null, null,
null, false);
}
}
@@ -0,0 +1,30 @@
/**
* 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.security.exception;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
@Getter
public class SqlValidationException extends AbstractPlatformDomainRuleException {
public SqlValidationException(final String message) {
super("error.msg.sql.validation", "SQL validation error: " + message);
}
}
@@ -0,0 +1,28 @@
/**
* 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.security.service;
import org.apache.fineract.infrastructure.security.exception.SqlValidationException;
public interface SqlValidator {
void validate(String statement) throws SqlValidationException;
void validate(String profile, String statement) throws SqlValidationException;
}
@@ -31,25 +31,22 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.stereotype.Component;
@Slf4j
@RequiredArgsConstructor
@Component
public class ColumnValidator {
private static final Logger LOG = LoggerFactory.getLogger(ColumnValidator.class);
private final SqlValidator sqlValidator;
private final JdbcTemplate jdbcTemplate;
@Autowired
public ColumnValidator(final JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "TODO: fix this!")
private void validateColumn(Map<String, Set<String>> tableColumnMap) {
Connection connection = null;
@@ -87,7 +84,7 @@ public class ColumnValidator {
columns.add(rs.getString("column_name"));
}
} catch (SQLException e) {
LOG.error("Problem occurred in getTableColumns function", e);
log.error("Problem occurred in getTableColumns function", e);
}
return columns;
}
@@ -97,7 +94,7 @@ public class ColumnValidator {
if (StringUtils.isBlank(condition)) {
continue;
}
SQLInjectionValidator.validateSQLInput(condition);
sqlValidator.validate("column", condition);
List<String> operator = new ArrayList<>(Arrays.asList("=", ">", "<", "> =", "< =", "! =", "!=", ">=", "<="));
condition = condition.trim().replace("( ", "(").replace(" )", ")").toLowerCase();
for (String op : operator) {
@@ -0,0 +1,100 @@
/**
* 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.security.utils;
import jakarta.annotation.PostConstruct;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.security.exception.SqlValidationException;
import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.springframework.stereotype.Component;
@Slf4j
@RequiredArgsConstructor
@Component
public class DefaultSqlValidator implements SqlValidator {
private final FineractProperties properties;
private Map<String, Pattern> patterns = new LinkedHashMap<>();
private Map<String, FineractProperties.FineractSqlValidationProfileProperties> profiles = new LinkedHashMap<>();
private static final String MAIN_PROFILE = "main";
@PostConstruct
public void init() {
properties.getSqlValidation().getPatterns().forEach(pattern -> {
log.info("Setup SQL validation pattern: {}", pattern.getName());
patterns.put(pattern.getName(), Pattern.compile(pattern.getPattern(), Pattern.DOTALL));
});
properties.getSqlValidation().getProfiles().forEach(profile -> {
log.info("Setup SQL validation profile: {}", profile.getName());
profile.getPatternRefs()
.sort(Comparator.comparing(FineractProperties.FineractSqlValidationPatternReferenceProperties::getOrder));
profiles.put(profile.getName(), profile);
});
// consistency checks
if (!profiles.containsKey(MAIN_PROFILE)) {
throw new IllegalStateException(
"SQL validation profile 'main' missing. This validation profile is the default fallback and has to be provided. NOTE: YOU CANNOT DISABLE SQL VALIDATION!!!");
}
// the default profile needs at least one pattern reference
if (profiles.get(MAIN_PROFILE).getPatternRefs().isEmpty()) {
throw new IllegalStateException(
"SQL Validation pattern references in profile 'main' are empty. Please make sure there is at least one reference available. NOTE: YOU CANNOT DISABLE SQL VALIDATION!!!");
}
// the default profile needs to be enabled
profiles.get(MAIN_PROFILE).setEnabled(true);
}
@Override
public void validate(final String statement) throws SqlValidationException {
validate(MAIN_PROFILE, statement);
}
@Override
public void validate(final String profile, final String statement) throws SqlValidationException {
if (StringUtils.isBlank(statement)) {
return;
}
for (var ref : profiles.getOrDefault(profile, profiles.get(MAIN_PROFILE)).getPatternRefs()) {
Matcher matcher = patterns.get(ref.getName()).matcher(statement);
if (matcher.matches()) {
log.warn("SQL validation error: >> {} <<", statement);
throw new SqlValidationException(String.format("invalid SQL statement (detected '%s' pattern)", ref.getName()));
}
}
}
}
@@ -28,8 +28,8 @@ import java.util.regex.Pattern;
* Utility to assemble the WHERE clause of an SQL query without the risk of SQL injection.
*
* <p>
* When using this utility instead of manually assembling SQL queries, then {@link SQLInjectionValidator} should not be
* required anymore. (Correctly using this means only ever passing completely fixed String literals to .)
* When using this utility instead of manually assembling SQL queries, then {@link SqlValidator} should not be required
* anymore. (Correctly using this means only ever passing completely fixed String literals to .)
*
* @author Michael Vorburger <mike@vorburger.ch>
*/
@@ -21,6 +21,7 @@ package org.apache.fineract.infrastructure.security.utils;
import java.sql.SQLException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
@Deprecated
public class SQLInjectionException extends PlatformApiDataValidationException {
public SQLInjectionException() {
@@ -1,166 +0,0 @@
/**
* 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.security.utils;
import java.util.List;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
public final class SQLInjectionValidator {
private SQLInjectionValidator() {
}
private static final String[] DDL_COMMANDS = { "create", "drop", "alter", "truncate", "comment", "sleep" };
private static final String[] DML_COMMANDS = { "select", "insert", "update", "delete", "merge", "upsert", "call" };
private static final String[] COMMENTS = { "--", "({", "/*", "#" };
private static final String SQL_PATTERN = "[a-zA-Z_=,\\-:'!><.?\"`% ()0-9*\n\r]*";
// TODO: see here https://rails-sqli.org for and
// https://larrysteinle.com/2011/02/20/use-regular-expressions-to-detect-sql-code-injection more examples
private static final List<String> INJECTION_PATTERNS = List.of("(?i).*[or|and]\s*[\"']?-1[\"']?\\s*(-*).*",
"(?i).*\\s+[\"']?(\\d+)[\"']?\\s*=\\s*[\"']?(\\1)[\"']?\\s*(-*).*");
public static void validateSQLInput(final String sqlSearch) {
if (StringUtils.isBlank(sqlSearch)) {
return;
}
String lowerCaseSQL = sqlSearch.toLowerCase();
List<String[]> commandsList = List.of(DDL_COMMANDS, DML_COMMANDS, COMMENTS);
validateSQLCommands(lowerCaseSQL, commandsList, String::contains);
patternMatchSqlInjection(sqlSearch, lowerCaseSQL);
}
public static void validateAdhocQuery(final String sqlSearch) {
if (StringUtils.isBlank(sqlSearch)) {
return;
}
String lowerCaseSQL = sqlSearch.toLowerCase().trim();
validateSQLCommand(lowerCaseSQL, DDL_COMMANDS, String::startsWith);
validateSQLCommand(lowerCaseSQL, COMMENTS, String::contains);
// Removing the space before and after '=' operator
// String s = " \" OR 1 = 1"; For the cases like this
patternMatchSqlInjection(sqlSearch, lowerCaseSQL);
}
public static void validateDynamicQuery(final String sqlSearch) {
if (StringUtils.isBlank(sqlSearch)) {
return;
}
String lowerCaseSQL = sqlSearch.toLowerCase();
List<String[]> commandsList = List.of(DDL_COMMANDS, DML_COMMANDS, COMMENTS);
validateSQLCommands(lowerCaseSQL, commandsList, String::equals);
// Removing the space before and after '=' operator
// String s = " \" OR 1 = 1"; For the cases like this
patternMatchSqlInjection(sqlSearch, lowerCaseSQL);
}
private static void patternMatchSqlInjection(String sqlSearch, String lowerCaseSQL) {
// Removing the space before and after '=' operator
// String s = " \" OR 1 = 1"; For the cases like this
boolean injectionFound = false;
String inputSqlString = lowerCaseSQL.replaceAll("\\s*=\\s*", "=");
StringTokenizer tokenizer = new StringTokenizer(inputSqlString, " ");
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken().trim();
if (token.equals("'")) {
if (tokenizer.hasMoreElements()) {
String nextToken = tokenizer.nextToken().trim();
if (!nextToken.equals("'")) {
injectionFound = true;
break;
}
} else {
injectionFound = true;
break;
}
}
if (token.equals("\"")) {
if (tokenizer.hasMoreElements()) {
String nextToken = tokenizer.nextToken().trim();
if (!nextToken.equals("\"")) {
injectionFound = true;
break;
}
} else {
injectionFound = true;
break;
}
} else if (token.indexOf('=') > 0) {
StringTokenizer operatorToken = new StringTokenizer(token, "=");
String operand = operatorToken.nextToken().trim();
if (!operatorToken.hasMoreTokens()) {
injectionFound = true;
break;
}
String value = operatorToken.nextToken().trim();
if (operand.equals(value)) {
injectionFound = true;
break;
}
}
}
if (injectionFound) {
throw new SQLInjectionException();
}
for (String injectionPattern : INJECTION_PATTERNS) {
Pattern pattern = Pattern.compile(injectionPattern);
Matcher matcher = pattern.matcher(sqlSearch);
if (matcher.matches()) {
throw new SQLInjectionException();
}
}
Pattern pattern = Pattern.compile(SQL_PATTERN);
Matcher matcher = pattern.matcher(sqlSearch);
if (!matcher.matches()) {
throw new SQLInjectionException();
}
}
private static void validateSQLCommand(String lowerCaseSQL, String[] commands, SQLCommandCondition condition) {
for (String command : commands) {
if (condition.checkCondition(lowerCaseSQL, command)) {
throw new SQLInjectionException();
}
}
}
private static void validateSQLCommands(String lowerCaseSQL, List<String[]> commandsList, SQLCommandCondition condition) {
for (String[] commands : commandsList) {
validateSQLCommand(lowerCaseSQL, commands, condition);
}
}
}
@@ -29,19 +29,23 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.security.utils.SQLInjectionValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.springframework.stereotype.Component;
@Slf4j
@RequiredArgsConstructor
@Component
public class AdHocQueryDataValidator {
private final SqlValidator sqlValidator;
private final FromJsonHelper fromApiJsonHelper;
private static final Set<String> AD_HOC_SEARCH_QUERY_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(
AdHocQuerySearchConstants.entitiesParamName, AdHocQuerySearchConstants.loanStatusParamName,
@@ -66,11 +70,6 @@ public class AdHocQueryDataValidator {
AdHocQuerySearchConstants.arrearsLoanStatusOption, AdHocQuerySearchConstants.closedLoanStatusOption,
AdHocQuerySearchConstants.writeoffLoanStatusOption };
@Autowired
public AdHocQueryDataValidator(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper;
}
public void validateAdHocQueryParameters(final String json) {
if (StringUtils.isBlank(json)) {
@@ -229,7 +228,7 @@ public class AdHocQueryDataValidator {
String loanDateOption = null;
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.loanDateOptionParamName, element)) {
loanDateOption = this.fromApiJsonHelper.extractStringNamed(AdHocQuerySearchConstants.loanDateOptionParamName, element);
SQLInjectionValidator.validateSQLInput(loanDateOption);
sqlValidator.validate(loanDateOption);
}
LocalDate loanFromDate = null;
@@ -252,7 +251,7 @@ public class AdHocQueryDataValidator {
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.outStandingAmountPercentageConditionParamName, element)) {
outStandingAmountPercentageCondition = this.fromApiJsonHelper
.extractStringNamed(AdHocQuerySearchConstants.outStandingAmountPercentageConditionParamName, element);
SQLInjectionValidator.validateSQLInput(outStandingAmountPercentageCondition);
sqlValidator.validate(outStandingAmountPercentageCondition);
}
BigDecimal minOutStandingAmountPercentage = null;
@@ -283,7 +282,7 @@ public class AdHocQueryDataValidator {
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.outstandingAmountConditionParamName, element)) {
outstandingAmountCondition = this.fromApiJsonHelper
.extractStringNamed(AdHocQuerySearchConstants.outstandingAmountConditionParamName, element);
SQLInjectionValidator.validateSQLInput(outstandingAmountCondition);
sqlValidator.validate(outstandingAmountCondition);
}
BigDecimal minOutstandingAmount = null;
@@ -39,6 +39,8 @@ import java.util.Locale;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
@@ -50,30 +52,34 @@ import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecific
import org.apache.fineract.infrastructure.core.service.database.JdbcJavaType;
import org.apache.fineract.infrastructure.core.service.database.SqlOperator;
import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData;
import org.apache.fineract.infrastructure.security.utils.SQLInjectionValidator;
import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.apache.fineract.portfolio.search.data.ColumnFilterData;
import org.apache.fineract.portfolio.search.data.FilterData;
import org.springframework.jdbc.support.rowset.SqlRowSet;
import org.springframework.stereotype.Component;
public final class SearchUtil {
@Slf4j
@RequiredArgsConstructor
@Component
public class SearchUtil {
public static final int DEFAULT_PAGE_SIZE = 50;
private static final JsonParserHelper helper = new JsonParserHelper();
private SearchUtil() {}
private final SqlValidator sqlValidator;
@NotNull
public static Map<String, ResultsetColumnHeaderData> mapHeadersToName(@NotNull Collection<ResultsetColumnHeaderData> columnHeaders) {
public Map<String, ResultsetColumnHeaderData> mapHeadersToName(@NotNull Collection<ResultsetColumnHeaderData> columnHeaders) {
return columnHeaders.stream().collect(Collectors.toMap(ResultsetColumnHeaderData::getColumnName, e -> e));
}
public static ResultsetColumnHeaderData findFiltered(@NotNull Collection<ResultsetColumnHeaderData> columnHeaders,
public ResultsetColumnHeaderData findFiltered(@NotNull Collection<ResultsetColumnHeaderData> columnHeaders,
@NotNull Predicate<ResultsetColumnHeaderData> filter) {
return columnHeaders.stream().filter(filter).findFirst().orElse(null);
}
public static ResultsetColumnHeaderData getFiltered(@NotNull Collection<ResultsetColumnHeaderData> columnHeaders,
public ResultsetColumnHeaderData getFiltered(@NotNull Collection<ResultsetColumnHeaderData> columnHeaders,
@NotNull Predicate<ResultsetColumnHeaderData> filter) {
ResultsetColumnHeaderData filtered = findFiltered(columnHeaders, filter);
if (filtered == null) {
@@ -82,8 +88,8 @@ public final class SearchUtil {
return filtered;
}
public static void extractJsonResult(@NotNull SqlRowSet rowSet, @NotNull List<String> selectColumns,
@NotNull List<String> resultColumns, @NotNull List<JsonObject> results) {
public void extractJsonResult(@NotNull SqlRowSet rowSet, @NotNull List<String> selectColumns, @NotNull List<String> resultColumns,
@NotNull List<JsonObject> results) {
JsonObject json = new JsonObject();
for (int i = 0; i < selectColumns.size(); i++) {
Object rowValue = rowSet.getObject(selectColumns.get(i));
@@ -115,15 +121,15 @@ public final class SearchUtil {
}
@NotNull
public static List<String> validateToJdbcColumnNames(List<String> columns, Map<String, ResultsetColumnHeaderData> headersByName,
public List<String> validateToJdbcColumnNames(List<String> columns, Map<String, ResultsetColumnHeaderData> headersByName,
boolean allowEmpty) {
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) {
public List<ResultsetColumnHeaderData> validateToJdbcColumns(List<String> columns, Map<String, ResultsetColumnHeaderData> headersByName,
boolean allowEmpty) {
final List<ApiParameterError> errors = new ArrayList<>();
List<ResultsetColumnHeaderData> result = new ArrayList<>();
@@ -140,13 +146,13 @@ public final class SearchUtil {
return result;
}
public static String validateToJdbcColumnName(String column, Map<String, ResultsetColumnHeaderData> headersByName, boolean allowEmpty) {
public String validateToJdbcColumnName(String column, Map<String, ResultsetColumnHeaderData> headersByName, 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) {
public 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()) {
@@ -155,8 +161,8 @@ public final class SearchUtil {
return columnHeader;
}
private static ResultsetColumnHeaderData validateToJdbcColumnImpl(String column,
@NotNull Map<String, ResultsetColumnHeaderData> headersByName, @NotNull List<ApiParameterError> errors, boolean allowEmpty) {
private ResultsetColumnHeaderData validateToJdbcColumnImpl(String column, @NotNull Map<String, ResultsetColumnHeaderData> headersByName,
@NotNull List<ApiParameterError> errors, boolean allowEmpty) {
if (!allowEmpty && column == null) {
errors.add(parameterErrorWithValue("error.msg.column.empty", "Column filter is empty", API_PARAM_COLUMN, null));
}
@@ -173,9 +179,9 @@ public final class SearchUtil {
return columnHeader;
}
public static boolean buildQueryCondition(List<ColumnFilterData> columnFilters, @NotNull StringBuilder where,
@NotNull List<Object> params, String alias, Map<String, ResultsetColumnHeaderData> headersByName, String dateFormat,
String dateTimeFormat, Locale locale, boolean embedded, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
public boolean buildQueryCondition(List<ColumnFilterData> columnFilters, @NotNull StringBuilder where, @NotNull List<Object> params,
String alias, Map<String, ResultsetColumnHeaderData> headersByName, String dateFormat, String dateTimeFormat, Locale locale,
boolean embedded, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
if (columnFilters == null) {
return false;
}
@@ -192,7 +198,7 @@ public final class SearchUtil {
return added;
}
public static boolean buildFilterCondition(ColumnFilterData columnFilter, @NotNull StringBuilder where, @NotNull List<Object> params,
public boolean buildFilterCondition(ColumnFilterData columnFilter, @NotNull StringBuilder where, @NotNull List<Object> params,
String alias, Map<String, ResultsetColumnHeaderData> headersByName, String dateFormat, String dateTimeFormat, Locale locale,
boolean embedded, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
String columnName = columnFilter.getColumn();
@@ -221,9 +227,8 @@ public final class SearchUtil {
return size > 0;
}
public static void buildCondition(@NotNull String definition, JdbcJavaType columnType, @NotNull SqlOperator operator,
List<Object> values, @NotNull StringBuilder where, @NotNull List<Object> params, String alias,
@NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
public void buildCondition(@NotNull String definition, JdbcJavaType columnType, @NotNull SqlOperator operator, List<Object> values,
@NotNull StringBuilder where, @NotNull List<Object> params, String alias, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
int paramCount = values == null ? 0 : values.size();
where.append(operator.formatPlaceholder(sqlGenerator, definition, paramCount, alias));
if (values != null) {
@@ -231,13 +236,13 @@ public final class SearchUtil {
}
}
public static Object parseJdbcColumnValue(@NotNull ResultsetColumnHeaderData columnHeader, String columnValue, String dateFormat,
public Object parseJdbcColumnValue(@NotNull ResultsetColumnHeaderData columnHeader, String columnValue, String dateFormat,
String dateTimeFormat, Locale locale, boolean strict, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
return columnHeader.getColumnType().toJdbcValue(sqlGenerator.getDialect(),
parseColumnValue(columnHeader, columnValue, dateFormat, dateTimeFormat, locale, strict, sqlGenerator), false);
}
public static Object parseColumnValue(@NotNull ResultsetColumnHeaderData columnHeader, String columnValue, String dateFormat,
public Object parseColumnValue(@NotNull ResultsetColumnHeaderData columnHeader, String columnValue, String dateFormat,
String dateTimeFormat, Locale locale, boolean strict, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
JdbcJavaType colType = columnHeader.getColumnType();
if (!colType.isStringType() || !columnHeader.isMandatory()) {
@@ -254,7 +259,7 @@ public final class SearchUtil {
return columnValue;
}
if (strict) {
SQLInjectionValidator.validateDynamicQuery(columnValue);
sqlValidator.validate(columnValue);
}
if (columnHeader.hasColumnValues()) {
@@ -314,7 +319,7 @@ public final class SearchUtil {
return columnValue;
}
public static String camelToSnake(final String camelStr) {
public String camelToSnake(final String camelStr) {
return camelStr == null ? null
: camelStr.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2").replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase();
}