FINERACT-1724: Further fixes

This commit is contained in:
Arnold Galovics
2023-06-06 11:45:08 +02:00
committed by Arnold Gálovics
parent a7d5f834d5
commit aa249cc16f
18 changed files with 454 additions and 304 deletions
@@ -26,7 +26,6 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
@@ -61,7 +61,7 @@ import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.context.SecurityContextHolderFilter;
@Configuration(proxyBeanMethods = false)
@Configuration
@ConditionalOnProperty("fineract.security.oauth.enabled")
@EnableMethodSecurity
public class OAuth2SecurityConfig {
@@ -23,8 +23,6 @@ import static org.springframework.security.authorization.AuthenticatedAuthorizat
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasAuthority;
import static org.springframework.security.authorization.AuthorizationManagers.allOf;
import org.apache.fineract.cob.service.InlineLoanCOBExecutorServiceImpl;
import org.apache.fineract.cob.service.LoanAccountLockService;
import org.apache.fineract.commands.domain.CommandSourceRepository;
import org.apache.fineract.commands.service.CommandSourceService;
import org.apache.fineract.infrastructure.businessdate.service.BusinessDateReadPlatformService;
@@ -33,12 +31,14 @@ import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDoma
import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
import org.apache.fineract.infrastructure.core.filters.CorrelationHeaderFilter;
import org.apache.fineract.infrastructure.core.filters.IdempotencyStoreFilter;
import org.apache.fineract.infrastructure.core.filters.IdempotencyStoreHelper;
import org.apache.fineract.infrastructure.core.filters.RequestResponseFilter;
import org.apache.fineract.infrastructure.core.filters.ResponseCorsFilter;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.service.MDCWrapper;
import org.apache.fineract.infrastructure.instancemode.filter.FineractInstanceModeApiFilter;
import org.apache.fineract.infrastructure.jobs.filter.LoanCOBApiFilter;
import org.apache.fineract.infrastructure.jobs.filter.LoanCOBFilterHelper;
import org.apache.fineract.infrastructure.security.data.PlatformRequestLog;
import org.apache.fineract.infrastructure.security.filter.InsecureTwoFactorAuthenticationFilter;
import org.apache.fineract.infrastructure.security.filter.TenantAwareBasicAuthenticationFilter;
@@ -48,9 +48,6 @@ import org.apache.fineract.infrastructure.security.service.PlatformSecurityConte
import org.apache.fineract.infrastructure.security.service.TenantAwareJpaPlatformUserDetailsService;
import org.apache.fineract.infrastructure.security.service.TwoFactorService;
import org.apache.fineract.notification.service.UserNotificationService;
import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequestRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.ServerProperties;
@@ -69,10 +66,10 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
import org.springframework.security.web.context.SecurityContextHolderFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration(proxyBeanMethods = false)
@Configuration
@ConditionalOnProperty("fineract.security.basicauth.enabled")
@EnableMethodSecurity
public class SecurityConfig {
@@ -110,19 +107,11 @@ public class SecurityConfig {
private FineractRequestContextHolder fineractRequestContextHolder;
@Autowired
private GLIMAccountInfoRepository glimAccountInfoRepository;
@Autowired
private LoanAccountLockService loanAccountLockService;
private LoanCOBFilterHelper loanCOBFilterHelper;
@Autowired
private PlatformSecurityContext context;
@Autowired
private InlineLoanCOBExecutorServiceImpl inlineLoanCOBExecutorService;
@Autowired
private LoanRepository loanRepository;
@Autowired
private LoanRescheduleRequestRepository loanRescheduleRequestRepository;
@Autowired
private PlatformTransactionManager transactionManager;
private IdempotencyStoreHelper idempotencyStoreHelper;
@Bean
public SecurityFilterChain authorizationFilterChain(HttpSecurity http) throws Exception {
@@ -152,14 +141,13 @@ public class SecurityConfig {
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http //
.csrf((csrf) -> csrf.disable()) // NOSONAR only creating a service that is used by non-browser clients
.securityContext((securityContext) -> securityContext.requireExplicitSave(false))
.sessionManagement((smc) -> smc.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) //
.addFilterBefore(tenantAwareBasicAuthenticationFilter(), SecurityContextHolderFilter.class)
.addFilterAfter(requestResponseFilter(), ExceptionTranslationFilter.class)
.addFilterAfter(correlationHeaderFilter(), RequestResponseFilter.class)
.addFilterAfter(responseCorsFilter(), CorrelationHeaderFilter.class) //
.addFilterAfter(fineractInstanceModeApiFilter(), ResponseCorsFilter.class) //
.addFilterAfter(tenantAwareBasicAuthenticationFilter(), FineractInstanceModeApiFilter.class) //
.addFilterAfter(loanCOBApiFilter(), TenantAwareBasicAuthenticationFilter.class) //
.addFilterAfter(loanCOBApiFilter(), FineractInstanceModeApiFilter.class) //
.addFilterAfter(idempotencyStoreFilter(), LoanCOBApiFilter.class); //
if (fineractProperties.getSecurity().getTwoFactor().isEnabled()) {
@@ -176,7 +164,7 @@ public class SecurityConfig {
}
public LoanCOBApiFilter loanCOBApiFilter() {
return new LoanCOBApiFilter(glimAccountInfoRepository, loanAccountLockService, context, inlineLoanCOBExecutorService, loanRepository, fineractProperties, loanRescheduleRequestRepository, transactionManager);
return new LoanCOBApiFilter(loanCOBFilterHelper);
}
public TwoFactorAuthenticationFilter twoFactorAuthenticationFilter() {
@@ -193,7 +181,7 @@ public class SecurityConfig {
}
public IdempotencyStoreFilter idempotencyStoreFilter() {
return new IdempotencyStoreFilter(commandSourceRepository, commandSourceService, fineractProperties, fineractRequestContextHolder);
return new IdempotencyStoreFilter(fineractRequestContextHolder, idempotencyStoreHelper, fineractProperties);
}
public CorrelationHeaderFilter correlationHeaderFilter() {
@@ -22,9 +22,11 @@ package org.apache.fineract.infrastructure.core.config;
import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.event.SimpleApplicationEventMulticaster;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
@Configuration
public class SpringConfig {
@@ -39,11 +41,17 @@ public class SpringConfig {
// The application events (for importing) rely on the inheritable thread local security context strategy
// This is NOT compatible with threadpools so if we use threadpools the below will need to be reworked
@Bean
public MethodInvokingFactoryBean methodInvokingFactoryBean() {
public MethodInvokingFactoryBean overrideSecurityContextHolderStrategy() {
MethodInvokingFactoryBean mifb = new MethodInvokingFactoryBean();
mifb.setTargetClass(SecurityContextHolder.class);
mifb.setTargetMethod("setStrategyName");
mifb.setArguments("MODE_INHERITABLETHREADLOCAL");
return mifb;
}
@Bean
@DependsOn("overrideSecurityContextHolderStrategy")
public SecurityContextHolderStrategy securityContextHolderStrategy() {
return SecurityContextHolder.getContextHolderStrategy();
}
}
@@ -0,0 +1,63 @@
/**
* 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.filters;
import jakarta.ws.rs.core.UriInfo;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse;
import org.apache.fineract.batch.domain.Header;
import org.apache.fineract.commands.service.SynchronousCommandProcessingService;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class IdempotencyStoreBatchFilter implements BatchFilter {
private final FineractRequestContextHolder fineractRequestContextHolder;
private final IdempotencyStoreHelper helper;
private final FineractProperties fineractProperties;
@Override
public BatchResponse doFilter(BatchRequest batchRequest, UriInfo uriInfo, BatchFilterChain chain) {
extractIdempotentKeyFromBatchRequest(batchRequest).ifPresent(idempotentKey -> fineractRequestContextHolder
.setAttribute(SynchronousCommandProcessingService.IDEMPOTENCY_KEY_ATTRIBUTE, idempotentKey));
BatchResponse result = chain.serviceCall(batchRequest, uriInfo);
Optional<Long> commandId = helper.getCommandId(null);
boolean isSuccessWithoutStored = helper.isStoreIdempotencyKey(null) && commandId.isPresent();
if (isSuccessWithoutStored) {
helper.storeCommandResult(true, result.getStatusCode(), result.getBody(), commandId);
}
return result;
}
private Optional<String> extractIdempotentKeyFromBatchRequest(BatchRequest request) {
if (request.getHeaders() == null) {
return Optional.empty();
}
return request.getHeaders() //
.stream().filter(header -> header.getName().equals(fineractProperties.getIdempotencyKeyHeaderName())) //
.map(Header::getValue) //
.findAny(); //
}
}
@@ -22,7 +22,6 @@ import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.ws.rs.core.UriInfo;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
@@ -30,11 +29,6 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.mutable.Mutable;
import org.apache.commons.lang3.mutable.MutableObject;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse;
import org.apache.fineract.batch.domain.Header;
import org.apache.fineract.commands.domain.CommandSourceRepository;
import org.apache.fineract.commands.service.CommandSourceService;
import org.apache.fineract.commands.service.SynchronousCommandProcessingService;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
@@ -44,31 +38,28 @@ import org.springframework.web.util.ContentCachingResponseWrapper;
@RequiredArgsConstructor
@Slf4j
public class IdempotencyStoreFilter extends OncePerRequestFilter implements BatchFilter {
private final CommandSourceRepository commandSourceRepository;
private final CommandSourceService commandSourceService;
private final FineractProperties fineractProperties;
public class IdempotencyStoreFilter extends OncePerRequestFilter {
private final FineractRequestContextHolder fineractRequestContextHolder;
private final IdempotencyStoreHelper helper;
private final FineractProperties fineractProperties;
@Override
protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,
@NotNull FilterChain filterChain) throws ServletException, IOException {
Mutable<ContentCachingResponseWrapper> wrapper = new MutableObject<>();
if (isAllowedContentTypeRequest(request)) {
if (helper.isAllowedContentTypeRequest(request)) {
wrapper.setValue(new ContentCachingResponseWrapper(response));
}
extractIdempotentKeyFromHttpServletRequest(request).ifPresent(idempotentKey -> fineractRequestContextHolder
.setAttribute(SynchronousCommandProcessingService.IDEMPOTENCY_KEY_ATTRIBUTE, idempotentKey, request));
filterChain.doFilter(request, wrapper.getValue() != null ? wrapper.getValue() : response);
Optional<Long> commandId = getCommandId(request);
boolean isSuccessWithoutStored = isStoreIdempotencyKey(request) && commandId.isPresent() && isAllowedContentTypeResponse(response)
Optional<Long> commandId = helper.getCommandId(request);
boolean isSuccessWithoutStored = helper.isStoreIdempotencyKey(request) && commandId.isPresent() && helper.isAllowedContentTypeResponse(response)
&& wrapper.getValue() != null;
if (isSuccessWithoutStored) {
storeCommandResult(false, response.getStatus(), Optional.ofNullable(wrapper.getValue())
helper.storeCommandResult(false, response.getStatus(), Optional.ofNullable(wrapper.getValue())
.map(ContentCachingResponseWrapper::getContentAsByteArray).map(s -> new String(s, StandardCharsets.UTF_8)).orElse(null),
commandId);
}
@@ -77,66 +68,7 @@ public class IdempotencyStoreFilter extends OncePerRequestFilter implements Batc
}
}
private void storeCommandResult(boolean batch, int response, String body, Optional<Long> commandId) {
commandSourceRepository.findById(commandId.get()).ifPresent(commandSource -> {
commandSource.setResultStatusCode(response);
commandSource.setResult(body);
if (batch) {
commandSourceService.saveResultNoTransaction(commandSource);
} else {
commandSourceService.saveResult(commandSource);
}
});
}
private Optional<String> extractIdempotentKeyFromHttpServletRequest(HttpServletRequest request) {
return Optional.ofNullable(request.getHeader(fineractProperties.getIdempotencyKeyHeaderName()));
}
private boolean isAllowedContentTypeResponse(HttpServletResponse response) {
return Optional.ofNullable(response.getContentType()).map(String::toLowerCase).map(ct -> ct.contains("application/json"))
.orElse(false) || (response.getStatus() > 200 && response.getStatus() < 300);
}
private boolean isAllowedContentTypeRequest(HttpServletRequest request) {
return Optional.ofNullable(request.getContentType()).map(String::toLowerCase).map(ct -> ct.contains("application/json"))
.orElse(false);
}
private boolean isStoreIdempotencyKey(HttpServletRequest request) {
return Optional
.ofNullable(
fineractRequestContextHolder.getAttribute(SynchronousCommandProcessingService.IDEMPOTENCY_KEY_STORE_FLAG, request))
.filter(Boolean.class::isInstance).map(Boolean.class::cast).orElse(false);
}
private Optional<Long> getCommandId(HttpServletRequest request) {
return Optional
.ofNullable(fineractRequestContextHolder.getAttribute(SynchronousCommandProcessingService.COMMAND_SOURCE_ID, request))
.filter(Long.class::isInstance).map(Long.class::cast);
}
private Optional<String> extractIdempotentKeyFromBatchRequest(BatchRequest request) {
if (request.getHeaders() == null) {
return Optional.empty();
}
return request.getHeaders() //
.stream().filter(header -> header.getName().equals(fineractProperties.getIdempotencyKeyHeaderName())) //
.map(Header::getValue) //
.findAny(); //
}
@Override
public BatchResponse doFilter(BatchRequest batchRequest, UriInfo uriInfo, BatchFilterChain chain) {
extractIdempotentKeyFromBatchRequest(batchRequest).ifPresent(idempotentKey -> fineractRequestContextHolder
.setAttribute(SynchronousCommandProcessingService.IDEMPOTENCY_KEY_ATTRIBUTE, idempotentKey));
BatchResponse result = chain.serviceCall(batchRequest, uriInfo);
Optional<Long> commandId = getCommandId(null);
boolean isSuccessWithoutStored = isStoreIdempotencyKey(null) && commandId.isPresent();
if (isSuccessWithoutStored) {
storeCommandResult(true, result.getStatusCode(), result.getBody(), commandId);
}
return result;
}
}
@@ -0,0 +1,73 @@
/**
* 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.filters;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandSourceRepository;
import org.apache.fineract.commands.service.CommandSourceService;
import org.apache.fineract.commands.service.SynchronousCommandProcessingService;
import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class IdempotencyStoreHelper {
private final CommandSourceRepository commandSourceRepository;
private final CommandSourceService commandSourceService;
private final FineractRequestContextHolder fineractRequestContextHolder;
public void storeCommandResult(boolean batch, int response, String body, Optional<Long> commandId) {
commandSourceRepository.findById(commandId.get()).ifPresent(commandSource -> {
commandSource.setResultStatusCode(response);
commandSource.setResult(body);
if (batch) {
commandSourceService.saveResultNoTransaction(commandSource);
} else {
commandSourceService.saveResult(commandSource);
}
});
}
public boolean isAllowedContentTypeResponse(HttpServletResponse response) {
return Optional.ofNullable(response.getContentType()).map(String::toLowerCase).map(ct -> ct.contains("application/json"))
.orElse(false) || (response.getStatus() > 200 && response.getStatus() < 300);
}
public boolean isAllowedContentTypeRequest(HttpServletRequest request) {
return Optional.ofNullable(request.getContentType()).map(String::toLowerCase).map(ct -> ct.contains("application/json"))
.orElse(false);
}
public boolean isStoreIdempotencyKey(HttpServletRequest request) {
return Optional
.ofNullable(
fineractRequestContextHolder.getAttribute(SynchronousCommandProcessingService.IDEMPOTENCY_KEY_STORE_FLAG, request))
.filter(Boolean.class::isInstance).map(Boolean.class::cast).orElse(false);
}
public Optional<Long> getCommandId(HttpServletRequest request) {
return Optional
.ofNullable(fineractRequestContextHolder.getAttribute(SynchronousCommandProcessingService.COMMAND_SOURCE_ID, request))
.filter(Long.class::isInstance).map(Long.class::cast);
}
}
@@ -160,6 +160,17 @@ public class DatabaseSpecificSQLGenerator {
}
}
public String castBigInt(String sql) {
if (databaseTypeResolver.isMySQL()) {
return format("CAST(%s AS BIGINT)", sql);
} else if (databaseTypeResolver.isPostgreSQL()) {
return format("%s::BIGINT", sql);
} else {
throw new IllegalStateException(
"Database type is not supported for casting to bigint " + databaseTypeResolver.databaseType());
}
}
public String currentSchema() {
if (databaseTypeResolver.isMySQL()) {
return "SCHEMA()";
@@ -30,6 +30,7 @@ import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver;
import org.apache.fineract.infrastructure.jobs.data.JobParameterDTO;
import org.springframework.beans.factory.InitializingBean;
@@ -43,6 +44,7 @@ public class JobExecutionRepository implements InitializingBean {
private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
private final FineractProperties fineractProperties;
private final DatabaseTypeResolver databaseTypeResolver;
private final DatabaseSpecificSQLGenerator sqlGenerator;
private final GoogleGsonSerializerHelper gsonFactory;
private Gson gson;
@@ -169,7 +171,7 @@ public class JobExecutionRepository implements InitializingBean {
String jsonString = gson.toJson(new JobParameterDTO(parameterKeyName, parameterValue));
sqlStatementBuilder.append(
"SELECT bje.JOB_EXECUTION_ID FROM BATCH_JOB_INSTANCE bji INNER JOIN BATCH_JOB_EXECUTION bje ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID INNER JOIN BATCH_JOB_EXECUTION_PARAMS bjep ON bje.JOB_EXECUTION_ID = bjep.JOB_EXECUTION_ID"
+ " WHERE bje.STATUS IN (:statuses) AND bji.JOB_NAME = :jobName AND bjep.KEY_NAME = :jobCustomParamKeyName AND bjep.LONG_VAL IN ("
+ " WHERE bje.STATUS IN (:statuses) AND bji.JOB_NAME = :jobName AND bjep.PARAMETER_NAME = :jobCustomParamKeyName AND " + sqlGenerator.castBigInt("bjep.PARAMETER_VALUE") + " IN ("
+ getSubQueryForCustomJobParameters()
+ ") AND bje.JOB_INSTANCE_ID NOT IN (SELECT bje.JOB_INSTANCE_ID FROM BATCH_JOB_INSTANCE bji INNER JOIN BATCH_JOB_EXECUTION bje ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID"
+ " WHERE bje.STATUS = :completedStatus AND bji.JOB_NAME = :jobName)");
@@ -18,73 +18,22 @@
*/
package org.apache.fineract.infrastructure.jobs.filter;
import static org.apache.fineract.batch.command.CommandStrategyUtils.isRelativeUrlVersioned;
import com.google.common.collect.Lists;
import io.github.resilience4j.core.functions.Either;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.cob.data.LoanIdAndLastClosedBusinessDate;
import org.apache.fineract.cob.service.InlineLoanCOBExecutorServiceImpl;
import org.apache.fineract.cob.service.LoanAccountLockService;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.filters.BatchRequestPreprocessor;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.jobs.exception.LoanIdsHardLockedException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository;
import org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonitoringAccount;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequestRepository;
import org.apache.fineract.useradministration.exception.UnAuthenticatedUserException;
import org.apache.http.HttpStatus;
import org.springframework.http.HttpMethod;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.filter.OncePerRequestFilter;
@RequiredArgsConstructor
public class LoanCOBApiFilter extends OncePerRequestFilter implements BatchRequestPreprocessor {
private final GLIMAccountInfoRepository glimAccountInfoRepository;
private final LoanAccountLockService loanAccountLockService;
private final PlatformSecurityContext context;
private final InlineLoanCOBExecutorServiceImpl inlineLoanCOBExecutorService;
private final LoanRepository loanRepository;
private final FineractProperties fineractProperties;
private final LoanRescheduleRequestRepository loanRescheduleRequestRepository;
private static final List<HttpMethod> HTTP_METHODS = List.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE);
public static final Pattern IGNORE_LOAN_PATH_PATTERN = Pattern.compile("/v[1-9][0-9]*/loans/catch-up");
public static final Pattern LOAN_PATH_PATTERN = Pattern.compile("/v[1-9][0-9]*/(?:reschedule)?loans/(?:external-id/)?([^/?]+).*");
public static final Pattern LOAN_GLIMACCOUNT_PATH_PATTERN = Pattern.compile("/v[1-9][0-9]*/loans/glimAccount/(\\d+).*");
private static final Predicate<String> URL_FUNCTION = s -> LOAN_PATH_PATTERN.matcher(s).find()
|| LOAN_GLIMACCOUNT_PATH_PATTERN.matcher(s).find();
private static final String JOB_NAME = "INLINE_LOAN_COB";
private final PlatformTransactionManager transactionManager;
public class LoanCOBApiFilter extends OncePerRequestFilter {
private final LoanCOBFilterHelper helper;
private static class Reject {
@@ -109,18 +58,18 @@ public class LoanCOBApiFilter extends OncePerRequestFilter implements BatchReque
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!isOnApiList(request.getPathInfo(), request.getMethod())) {
if (!helper.isOnApiList(request.getPathInfo(), request.getMethod())) {
proceed(filterChain, request, response);
} else {
try {
boolean bypassUser = isBypassUser();
boolean bypassUser = helper.isBypassUser();
if (bypassUser) {
proceed(filterChain, request, response);
} else {
try {
List<Long> loanIds = calculateRelevantLoanIds(request.getPathInfo());
if (!loanIds.isEmpty() && isLoanBehind(loanIds)) {
executeInlineCob(loanIds);
List<Long> loanIds = helper.calculateRelevantLoanIds(request.getPathInfo());
if (!loanIds.isEmpty() && helper.isLoanBehind(loanIds)) {
helper.executeInlineCob(loanIds);
}
proceed(filterChain, request, response);
} catch (LoanIdsHardLockedException e) {
@@ -133,130 +82,9 @@ public class LoanCOBApiFilter extends OncePerRequestFilter implements BatchReque
}
}
private boolean isLoanBehind(List<Long> loanIds) {
List<LoanIdAndLastClosedBusinessDate> loanIdAndLastClosedBusinessDates = new ArrayList<>();
List<List<Long>> partitions = Lists.partition(loanIds, fineractProperties.getQuery().getInClauseParameterSizeLimit());
partitions.forEach(partition -> loanIdAndLastClosedBusinessDates.addAll(loanRepository
.findAllNonClosedLoansBehindByLoanIds(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE), partition)));
return CollectionUtils.isNotEmpty(loanIdAndLastClosedBusinessDates);
}
private List<Long> calculateRelevantLoanIds(String pathInfo) {
List<Long> loanIds = getLoanIdList(pathInfo);
if (isLoanHardLocked(loanIds)) {
throw new LoanIdsHardLockedException(loanIds.get(0));
} else {
return loanIds;
}
}
private List<Long> getLoanIdList(String pathInfo) {
boolean isGlim = isGlim(pathInfo);
Long loanIdFromRequest = getLoanId(isGlim, pathInfo);
if (loanIdFromRequest == null) {
return Collections.emptyList();
}
if (isGlim) {
return getGlimChildLoanIds(loanIdFromRequest);
} else {
return Collections.singletonList(loanIdFromRequest);
}
}
private void executeInlineCob(List<Long> loanIds) {
inlineLoanCOBExecutorService.execute(loanIds, JOB_NAME);
}
private boolean isBypassUser() {
return context.authenticatedUser().isBypassUser();
}
private List<Long> getGlimChildLoanIds(Long loanIdFromRequest) {
GroupLoanIndividualMonitoringAccount glimAccount = glimAccountInfoRepository.findOneByIsAcceptingChildAndApplicationId(true,
BigDecimal.valueOf(loanIdFromRequest));
if (glimAccount != null) {
return glimAccount.getChildLoan().stream().map(Loan::getId).toList();
} else {
return Collections.emptyList();
}
}
private boolean isLoanHardLocked(List<Long> loanIds) {
return loanIds.stream().anyMatch(loanAccountLockService::isLoanHardLocked);
}
private void proceed(FilterChain filterChain, HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
filterChain.doFilter(request, response);
}
private Long getLoanId(boolean isGlim, String pathInfo) {
if (!isGlim) {
String id = LOAN_PATH_PATTERN.matcher(pathInfo).replaceAll("$1");
if (isExternal(pathInfo)) {
String externalId = id;
return loanRepository.findIdByExternalId(new ExternalId(externalId));
} else if (isRescheduleLoans(pathInfo)) {
return loanRescheduleRequestRepository.getLoanIdByRescheduleRequestId(Long.valueOf(id)).orElse(null);
} else if (StringUtils.isNumeric(id)) {
return Long.valueOf(id);
} else {
return null;
}
} else {
return Long.valueOf(LOAN_GLIMACCOUNT_PATH_PATTERN.matcher(pathInfo).replaceAll("$1"));
}
}
private boolean isExternal(String pathInfo) {
return LOAN_PATH_PATTERN.matcher(pathInfo).matches() && pathInfo.contains("external-id");
}
private boolean isRescheduleLoans(String pathInfo) {
return LOAN_PATH_PATTERN.matcher(pathInfo).matches() && pathInfo.contains("/v1/rescheduleloans/");
}
private boolean isOnApiList(String pathInfo, String method) {
if (StringUtils.isBlank(pathInfo)) {
return false;
}
return HTTP_METHODS.contains(HttpMethod.valueOf(method)) && !IGNORE_LOAN_PATH_PATTERN.matcher(pathInfo).find()
&& URL_FUNCTION.test(pathInfo);
}
private boolean isGlim(String pathInfo) {
return LOAN_GLIMACCOUNT_PATH_PATTERN.matcher(pathInfo).matches();
}
@Override
public Either<RuntimeException, BatchRequest> preprocess(BatchRequest batchRequest) {
TransactionTemplate tr = new TransactionTemplate(transactionManager);
tr.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED);
return tr.execute(status -> {
try {
String method = batchRequest.getMethod();
String relativeUrl = "/" + batchRequest.getRelativeUrl();
if (!isRelativeUrlVersioned(batchRequest.getRelativeUrl())) {
// to support pre-versioned relative paths
relativeUrl = "/v1/" + batchRequest.getRelativeUrl();
}
if (isOnApiList(relativeUrl, method)) {
boolean bypassUser = isBypassUser();
if (!bypassUser) {
List<Long> result = calculateRelevantLoanIds(relativeUrl);
if (!result.isEmpty() && isLoanBehind(result)) {
executeInlineCob(result);
}
}
}
} catch (LoanNotFoundException e) {
return Either.right(batchRequest);
} catch (RuntimeException e) {
return Either.left(e);
}
return Either.right(batchRequest);
});
}
}
@@ -0,0 +1,70 @@
/**
* 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.jobs.filter;
import static org.apache.fineract.batch.command.CommandStrategyUtils.isRelativeUrlVersioned;
import io.github.resilience4j.core.functions.Either;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.infrastructure.core.filters.BatchRequestPreprocessor;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
@Component
@RequiredArgsConstructor
public class LoanCOBBatchPreprocessor implements BatchRequestPreprocessor {
private final LoanCOBFilterHelper helper;
private final PlatformTransactionManager transactionManager;
@Override
public Either<RuntimeException, BatchRequest> preprocess(BatchRequest batchRequest) {
TransactionTemplate tr = new TransactionTemplate(transactionManager);
tr.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED);
return tr.execute(status -> {
try {
String method = batchRequest.getMethod();
String relativeUrl = "/" + batchRequest.getRelativeUrl();
if (!isRelativeUrlVersioned(batchRequest.getRelativeUrl())) {
// to support pre-versioned relative paths
relativeUrl = "/v1/" + batchRequest.getRelativeUrl();
}
if (helper.isOnApiList(relativeUrl, method)) {
boolean bypassUser = helper.isBypassUser();
if (!bypassUser) {
List<Long> result = helper.calculateRelevantLoanIds(relativeUrl);
if (!result.isEmpty() && helper.isLoanBehind(result)) {
helper.executeInlineCob(result);
}
}
}
} catch (LoanNotFoundException e) {
return Either.right(batchRequest);
} catch (RuntimeException e) {
return Either.left(e);
}
return Either.right(batchRequest);
});
}
}
@@ -0,0 +1,160 @@
/**
* 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.jobs.filter;
import com.google.common.collect.Lists;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.cob.data.LoanIdAndLastClosedBusinessDate;
import org.apache.fineract.cob.service.InlineLoanCOBExecutorServiceImpl;
import org.apache.fineract.cob.service.LoanAccountLockService;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.jobs.exception.LoanIdsHardLockedException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository;
import org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonitoringAccount;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequestRepository;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
@RequiredArgsConstructor
@Component
public class LoanCOBFilterHelper {
private final GLIMAccountInfoRepository glimAccountInfoRepository;
private final LoanAccountLockService loanAccountLockService;
private final PlatformSecurityContext context;
private final InlineLoanCOBExecutorServiceImpl inlineLoanCOBExecutorService;
private final LoanRepository loanRepository;
private final FineractProperties fineractProperties;
private final LoanRescheduleRequestRepository loanRescheduleRequestRepository;
private static final List<HttpMethod> HTTP_METHODS = List.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE);
public static final Pattern IGNORE_LOAN_PATH_PATTERN = Pattern.compile("/v[1-9][0-9]*/loans/catch-up");
public static final Pattern LOAN_PATH_PATTERN = Pattern.compile("/v[1-9][0-9]*/(?:reschedule)?loans/(?:external-id/)?([^/?]+).*");
public static final Pattern LOAN_GLIMACCOUNT_PATH_PATTERN = Pattern.compile("/v[1-9][0-9]*/loans/glimAccount/(\\d+).*");
private static final Predicate<String> URL_FUNCTION = s -> LOAN_PATH_PATTERN.matcher(s).find()
|| LOAN_GLIMACCOUNT_PATH_PATTERN.matcher(s).find();
private static final String JOB_NAME = "INLINE_LOAN_COB";
private Long getLoanId(boolean isGlim, String pathInfo) {
if (!isGlim) {
String id = LOAN_PATH_PATTERN.matcher(pathInfo).replaceAll("$1");
if (isExternal(pathInfo)) {
String externalId = id;
return loanRepository.findIdByExternalId(new ExternalId(externalId));
} else if (isRescheduleLoans(pathInfo)) {
return loanRescheduleRequestRepository.getLoanIdByRescheduleRequestId(Long.valueOf(id)).orElse(null);
} else if (StringUtils.isNumeric(id)) {
return Long.valueOf(id);
} else {
return null;
}
} else {
return Long.valueOf(LOAN_GLIMACCOUNT_PATH_PATTERN.matcher(pathInfo).replaceAll("$1"));
}
}
private boolean isExternal(String pathInfo) {
return LOAN_PATH_PATTERN.matcher(pathInfo).matches() && pathInfo.contains("external-id");
}
private boolean isRescheduleLoans(String pathInfo) {
return LOAN_PATH_PATTERN.matcher(pathInfo).matches() && pathInfo.contains("/v1/rescheduleloans/");
}
public boolean isOnApiList(String pathInfo, String method) {
if (StringUtils.isBlank(pathInfo)) {
return false;
}
return HTTP_METHODS.contains(HttpMethod.valueOf(method)) && !IGNORE_LOAN_PATH_PATTERN.matcher(pathInfo).find()
&& URL_FUNCTION.test(pathInfo);
}
private boolean isGlim(String pathInfo) {
return LOAN_GLIMACCOUNT_PATH_PATTERN.matcher(pathInfo).matches();
}
public boolean isBypassUser() {
return context.authenticatedUser().isBypassUser();
}
private List<Long> getGlimChildLoanIds(Long loanIdFromRequest) {
GroupLoanIndividualMonitoringAccount glimAccount = glimAccountInfoRepository.findOneByIsAcceptingChildAndApplicationId(true,
BigDecimal.valueOf(loanIdFromRequest));
if (glimAccount != null) {
return glimAccount.getChildLoan().stream().map(Loan::getId).toList();
} else {
return Collections.emptyList();
}
}
private boolean isLoanHardLocked(List<Long> loanIds) {
return loanIds.stream().anyMatch(loanAccountLockService::isLoanHardLocked);
}
public boolean isLoanBehind(List<Long> loanIds) {
List<LoanIdAndLastClosedBusinessDate> loanIdAndLastClosedBusinessDates = new ArrayList<>();
List<List<Long>> partitions = Lists.partition(loanIds, fineractProperties.getQuery().getInClauseParameterSizeLimit());
partitions.forEach(partition -> loanIdAndLastClosedBusinessDates.addAll(loanRepository
.findAllNonClosedLoansBehindByLoanIds(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE), partition)));
return CollectionUtils.isNotEmpty(loanIdAndLastClosedBusinessDates);
}
public List<Long> calculateRelevantLoanIds(String pathInfo) {
List<Long> loanIds = getLoanIdList(pathInfo);
if (isLoanHardLocked(loanIds)) {
throw new LoanIdsHardLockedException(loanIds.get(0));
} else {
return loanIds;
}
}
private List<Long> getLoanIdList(String pathInfo) {
boolean isGlim = isGlim(pathInfo);
Long loanIdFromRequest = getLoanId(isGlim, pathInfo);
if (loanIdFromRequest == null) {
return Collections.emptyList();
}
if (isGlim) {
return getGlimChildLoanIds(loanIdFromRequest);
} else {
return Collections.singletonList(loanIdFromRequest);
}
}
public void executeInlineCob(List<Long> loanIds) {
inlineLoanCOBExecutorService.execute(loanIds, JOB_NAME);
}
}
@@ -44,8 +44,10 @@ import org.mockito.quality.Strictness;
import org.springframework.batch.core.configuration.ListableJobLocator;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.launch.JobOperator;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration;
import org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
@@ -72,7 +74,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class, GsonAutoConfiguration.class, JdbcTemplateAutoConfiguration.class,
LiquibaseAutoConfiguration.class })
LiquibaseAutoConfiguration.class, BatchAutoConfiguration.class })
@EnableTransactionManagement
@EnableWebSecurity
@EnableConfigurationProperties({ FineractProperties.class, LiquibaseProperties.class })
@@ -180,4 +182,10 @@ public class TestConfiguration {
public JobRepository jobRepository() {
return mock(JobRepository.class, RETURNS_MOCKS);
}
@Primary
@Bean
public JobOperator jobOperator() {
return mock(JobOperator.class, RETURNS_MOCKS);
}
}
@@ -18,6 +18,8 @@
*/
package org.apache.fineract.infrastructure.jobs.filter;
import static org.apache.fineract.infrastructure.jobs.filter.LoanCOBFilterHelper.LOAN_GLIMACCOUNT_PATH_PATTERN;
import static org.apache.fineract.infrastructure.jobs.filter.LoanCOBFilterHelper.LOAN_PATH_PATTERN;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.eq;
@@ -27,6 +29,8 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import com.sun.research.ws.wadl.HTTPMethods;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import java.io.IOException;
import java.io.PrintWriter;
import java.math.BigDecimal;
@@ -36,8 +40,6 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Optional;
import java.util.UUID;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import org.apache.fineract.cob.data.LoanIdAndLastClosedBusinessDate;
import org.apache.fineract.cob.service.InlineLoanCOBExecutorServiceImpl;
import org.apache.fineract.cob.service.LoanAccountLockService;
@@ -55,6 +57,7 @@ import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanResch
import org.apache.fineract.useradministration.domain.AppUser;
import org.apache.http.HttpStatus;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -69,8 +72,9 @@ import org.springframework.mock.web.MockHttpServletResponse;
@MockitoSettings(strictness = Strictness.LENIENT)
class LoanCOBApiFilterTest {
@InjectMocks
private LoanCOBApiFilter testObj;
@InjectMocks
private LoanCOBFilterHelper helper;
@Mock
private LoanAccountLockService loanAccountLockService;
@Mock
@@ -85,41 +89,45 @@ class LoanCOBApiFilterTest {
private FineractProperties fineractProperties;
@Mock
private FineractProperties.FineractQueryProperties fineractQueryProperties;
@Mock
private LoanRescheduleRequestRepository loanRescheduleRequestRepository;
@BeforeEach
public void setUp() {
testObj = new LoanCOBApiFilter(helper);
}
@Test
void shouldLoanAndExternalMatchToo() {
String externalId = UUID.randomUUID().toString();
Assertions.assertTrue(LoanCOBApiFilter.LOAN_PATH_PATTERN.matcher("/v1/loans/12").matches());
Assertions.assertTrue(LoanCOBApiFilter.LOAN_PATH_PATTERN.matcher("/v1/loans/12?correct=parameter").matches());
Assertions.assertTrue(LoanCOBApiFilter.LOAN_PATH_PATTERN.matcher("/v1/loans/12?correct=parameter").matches());
Assertions.assertTrue(LoanCOBApiFilter.LOAN_PATH_PATTERN.matcher("/v1/rescheduleloans/12").matches());
Assertions.assertTrue(LoanCOBApiFilter.LOAN_PATH_PATTERN.matcher("/v1/rescheduleloans/12?correct=parameter").matches());
Assertions.assertTrue(LoanCOBApiFilter.LOAN_PATH_PATTERN.matcher("/v1/rescheduleloans/12?correct=parameter").matches());
Assertions.assertTrue(LoanCOBApiFilter.LOAN_PATH_PATTERN.matcher("/v1/loans/external-id/" + externalId).matches());
Assertions.assertTrue(LOAN_PATH_PATTERN.matcher("/v1/loans/12").matches());
Assertions.assertTrue(LOAN_PATH_PATTERN.matcher("/v1/loans/12?correct=parameter").matches());
Assertions.assertTrue(LOAN_PATH_PATTERN.matcher("/v1/loans/12?correct=parameter").matches());
Assertions.assertTrue(LOAN_PATH_PATTERN.matcher("/v1/rescheduleloans/12").matches());
Assertions.assertTrue(LOAN_PATH_PATTERN.matcher("/v1/rescheduleloans/12?correct=parameter").matches());
Assertions.assertTrue(LOAN_PATH_PATTERN.matcher("/v1/rescheduleloans/12?correct=parameter").matches());
Assertions.assertTrue(LOAN_PATH_PATTERN.matcher("/v1/loans/external-id/" + externalId).matches());
Assertions.assertTrue(
LoanCOBApiFilter.LOAN_PATH_PATTERN.matcher("/v1/loans/external-id/" + externalId + "?additional=parameter").matches());
Assertions.assertEquals("12", LoanCOBApiFilter.LOAN_PATH_PATTERN.matcher("/v1/loans/12").replaceAll("$1"));
Assertions.assertEquals("12", LoanCOBApiFilter.LOAN_PATH_PATTERN.matcher("/v1/loans/12?correct=parameter").replaceAll("$1"));
Assertions.assertEquals("12", LoanCOBApiFilter.LOAN_PATH_PATTERN.matcher("/v1/rescheduleloans/12").replaceAll("$1"));
LOAN_PATH_PATTERN.matcher("/v1/loans/external-id/" + externalId + "?additional=parameter").matches());
Assertions.assertEquals("12", LOAN_PATH_PATTERN.matcher("/v1/loans/12").replaceAll("$1"));
Assertions.assertEquals("12", LOAN_PATH_PATTERN.matcher("/v1/loans/12?correct=parameter").replaceAll("$1"));
Assertions.assertEquals("12", LOAN_PATH_PATTERN.matcher("/v1/rescheduleloans/12").replaceAll("$1"));
Assertions.assertEquals("12",
LoanCOBApiFilter.LOAN_PATH_PATTERN.matcher("/v1/rescheduleloans/12?correct=parameter").replaceAll("$1"));
LOAN_PATH_PATTERN.matcher("/v1/rescheduleloans/12?correct=parameter").replaceAll("$1"));
Assertions.assertEquals(externalId,
LoanCOBApiFilter.LOAN_PATH_PATTERN.matcher("/v1/loans/external-id/" + externalId).replaceAll("$1"));
Assertions.assertEquals(externalId, LoanCOBApiFilter.LOAN_PATH_PATTERN
LOAN_PATH_PATTERN.matcher("/v1/loans/external-id/" + externalId).replaceAll("$1"));
Assertions.assertEquals(externalId, LOAN_PATH_PATTERN
.matcher("/v1/loans/external-id/" + externalId + "?additional=parameter").replaceAll("$1"));
}
@Test
void shouldGlimAccountMatch() {
Assertions.assertTrue(LoanCOBApiFilter.LOAN_GLIMACCOUNT_PATH_PATTERN.matcher("/v1/loans/glimAccount/12").matches());
Assertions.assertTrue(LOAN_GLIMACCOUNT_PATH_PATTERN.matcher("/v1/loans/glimAccount/12").matches());
Assertions.assertTrue(
LoanCOBApiFilter.LOAN_GLIMACCOUNT_PATH_PATTERN.matcher("/v1/loans/glimAccount/12?additional=parameter").matches());
Assertions.assertEquals("12", LoanCOBApiFilter.LOAN_GLIMACCOUNT_PATH_PATTERN.matcher("/v1/loans/glimAccount/12").replaceAll("$1"));
LOAN_GLIMACCOUNT_PATH_PATTERN.matcher("/v1/loans/glimAccount/12?additional=parameter").matches());
Assertions.assertEquals("12", LOAN_GLIMACCOUNT_PATH_PATTERN.matcher("/v1/loans/glimAccount/12").replaceAll("$1"));
Assertions.assertEquals("12",
LoanCOBApiFilter.LOAN_GLIMACCOUNT_PATH_PATTERN.matcher("/v1/loans/glimAccount/12?additional=parameter").replaceAll("$1"));
LOAN_GLIMACCOUNT_PATH_PATTERN.matcher("/v1/loans/glimAccount/12?additional=parameter").replaceAll("$1"));
}
@Test
+1 -1
View File
@@ -53,5 +53,5 @@ dependencies {
testImplementation 'org.mapstruct:mapstruct'
testAnnotationProcessor 'org.mapstruct:mapstruct-processor'
testImplementation 'com.github.tomakehurst:wiremock-jre8:2.35.0'
testImplementation 'com.github.tomakehurst:wiremock:3.0.0-beta-8'
}