FINERACT-1724: Further fixes
This commit is contained in:
committed by
Arnold Gálovics
parent
a7d5f834d5
commit
aa249cc16f
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+10
-22
@@ -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() {
|
||||
|
||||
+9
-1
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+63
@@ -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(); //
|
||||
|
||||
}
|
||||
}
|
||||
+7
-75
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+73
@@ -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);
|
||||
}
|
||||
}
|
||||
+11
@@ -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()";
|
||||
|
||||
+3
-1
@@ -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)");
|
||||
|
||||
+7
-179
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+70
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
+160
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+30
-22
@@ -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
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Reference in New Issue
Block a user