This is an automated email from the ASF dual-hosted git repository. arnold pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/fineract.git
commit aa249cc16fafc91e616681311b6889a5d4d68bf7 Author: Arnold Galovics <[email protected]> AuthorDate: Fri Jun 2 14:00:41 2023 +0200 FINERACT-1724: Further fixes --- .../api/ExternalAssetOwnersApiResource.java | 1 - .../core/config/OAuth2SecurityConfig.java | 2 +- .../infrastructure/core/config/SecurityConfig.java | 32 +--- .../infrastructure/core/config/SpringConfig.java | 10 +- .../core/filters/IdempotencyStoreBatchFilter.java | 63 +++++++ .../core/filters/IdempotencyStoreFilter.java | 82 +-------- .../core/filters/IdempotencyStoreHelper.java | 73 ++++++++ .../database/DatabaseSpecificSQLGenerator.java | 11 ++ .../jobs/domain/JobExecutionRepository.java | 4 +- .../jobs/filter/LoanCOBApiFilter.java | 186 +------------------ .../jobs/filter/LoanCOBBatchPreprocessor.java | 70 +++++++ ...nCOBApiFilter.java => LoanCOBFilterHelper.java} | 202 +++++---------------- .../org/apache/fineract/TestConfiguration.java | 10 +- .../jobs/filter/LoanCOBApiFilterTest.java | 52 +++--- integration-tests/dependencies.gradle | 2 +- .../bulkimport/importhandler/loan/Loan.xls | Bin 2328576 -> 0 bytes .../bulkimport/importhandler/office/Office.xls | Bin 359936 -> 0 bytes .../bulkimport/importhandler/savings/Savings.xls | Bin 1843712 -> 0 bytes 18 files changed, 344 insertions(+), 456 deletions(-) diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResource.java b/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResource.java index c6b980a44..e1ae283d5 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResource.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResource.java @@ -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; diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/OAuth2SecurityConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/OAuth2SecurityConfig.java index 37caef0a9..a59b12375 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/OAuth2SecurityConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/OAuth2SecurityConfig.java @@ -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 { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java index 7fb093c6b..51ab4a148 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java @@ -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() { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SpringConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SpringConfig.java index ef6149fb5..f4ae4a5e4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SpringConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SpringConfig.java @@ -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(); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreBatchFilter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreBatchFilter.java new file mode 100644 index 000000000..b2e49222a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreBatchFilter.java @@ -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(); // + + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreFilter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreFilter.java index 6270b8e20..8a809b513 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreFilter.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreFilter.java @@ -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; - } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreHelper.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreHelper.java new file mode 100644 index 000000000..5ca5147a0 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreHelper.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGenerator.java index 57d511577..135c61dc9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGenerator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/database/DatabaseSpecificSQLGenerator.java @@ -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()"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/JobExecutionRepository.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/JobExecutionRepository.java index ca3750a11..8d633e1e7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/JobExecutionRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/JobExecutionRepository.java @@ -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)"); diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilter.java index 19d51caa0..650dcc428 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilter.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilter.java @@ -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); - }); - } - } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBBatchPreprocessor.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBBatchPreprocessor.java new file mode 100644 index 000000000..a7bacc237 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBBatchPreprocessor.java @@ -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); + }); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBFilterHelper.java similarity index 58% copy from fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilter.java copy to fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBFilterHelper.java index 19d51caa0..960808a9c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilter.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBFilterHelper.java @@ -18,15 +18,7 @@ */ 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; @@ -36,15 +28,12 @@ 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; @@ -52,19 +41,13 @@ import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepositor 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; +import org.springframework.stereotype.Component; @RequiredArgsConstructor -public class LoanCOBApiFilter extends OncePerRequestFilter implements BatchRequestPreprocessor { - +@Component +public class LoanCOBFilterHelper { private final GLIMAccountInfoRepository glimAccountInfoRepository; private final LoanAccountLockService loanAccountLockService; private final PlatformSecurityContext context; @@ -82,58 +65,65 @@ public class LoanCOBApiFilter extends OncePerRequestFilter implements BatchReque 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; + 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 static class Reject { + private boolean isExternal(String pathInfo) { + return LOAN_PATH_PATTERN.matcher(pathInfo).matches() && pathInfo.contains("external-id"); + } - private final String message; - private final Integer statusCode; + private boolean isRescheduleLoans(String pathInfo) { + return LOAN_PATH_PATTERN.matcher(pathInfo).matches() && pathInfo.contains("/v1/rescheduleloans/"); + } - Reject(String message, Integer statusCode) { - this.message = message; - this.statusCode = statusCode; + 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); + } - public static Reject reject(Long loanId, int status) { - return new Reject(ApiGlobalErrorResponse.loanIsLocked(loanId).toJson(), status); - } + private boolean isGlim(String pathInfo) { + return LOAN_GLIMACCOUNT_PATH_PATTERN.matcher(pathInfo).matches(); + } - public void toServletResponse(HttpServletResponse response) throws IOException { - response.setStatus(statusCode); - response.getWriter().write(message); - } + public boolean isBypassUser() { + return context.authenticatedUser().isBypassUser(); } - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { - if (!isOnApiList(request.getPathInfo(), request.getMethod())) { - proceed(filterChain, request, response); + 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 { - try { - boolean bypassUser = isBypassUser(); - if (bypassUser) { - proceed(filterChain, request, response); - } else { - try { - List<Long> loanIds = calculateRelevantLoanIds(request.getPathInfo()); - if (!loanIds.isEmpty() && isLoanBehind(loanIds)) { - executeInlineCob(loanIds); - } - proceed(filterChain, request, response); - } catch (LoanIdsHardLockedException e) { - Reject.reject(e.getLoanIdFromRequest(), HttpStatus.SC_CONFLICT).toServletResponse(response); - } - } - } catch (UnAuthenticatedUserException e) { - Reject.reject(null, HttpStatus.SC_UNAUTHORIZED).toServletResponse(response); - } + return Collections.emptyList(); } } - private boolean isLoanBehind(List<Long> loanIds) { + 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 @@ -141,7 +131,7 @@ public class LoanCOBApiFilter extends OncePerRequestFilter implements BatchReque return CollectionUtils.isNotEmpty(loanIdAndLastClosedBusinessDates); } - private List<Long> calculateRelevantLoanIds(String pathInfo) { + public List<Long> calculateRelevantLoanIds(String pathInfo) { List<Long> loanIds = getLoanIdList(pathInfo); if (isLoanHardLocked(loanIds)) { @@ -164,99 +154,7 @@ public class LoanCOBApiFilter extends OncePerRequestFilter implements BatchReque } } - private void executeInlineCob(List<Long> loanIds) { + public 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); - }); - } - } diff --git a/fineract-provider/src/test/java/org/apache/fineract/TestConfiguration.java b/fineract-provider/src/test/java/org/apache/fineract/TestConfiguration.java index 1fb160834..8eb9a36dc 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/TestConfiguration.java +++ b/fineract-provider/src/test/java/org/apache/fineract/TestConfiguration.java @@ -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); + } } diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilterTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilterTest.java index 68726515f..c02a394fe 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilterTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilterTest.java @@ -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 diff --git a/integration-tests/dependencies.gradle b/integration-tests/dependencies.gradle index ee05c9657..d4b6f4db3 100644 --- a/integration-tests/dependencies.gradle +++ b/integration-tests/dependencies.gradle @@ -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' } diff --git a/integration-tests/src/integrationTest/resources/bulkimport/importhandler/loan/Loan.xls b/integration-tests/src/integrationTest/resources/bulkimport/importhandler/loan/Loan.xls deleted file mode 100644 index a7c87b5bd..000000000 Binary files a/integration-tests/src/integrationTest/resources/bulkimport/importhandler/loan/Loan.xls and /dev/null differ diff --git a/integration-tests/src/integrationTest/resources/bulkimport/importhandler/office/Office.xls b/integration-tests/src/integrationTest/resources/bulkimport/importhandler/office/Office.xls deleted file mode 100644 index 3a63e1221..000000000 Binary files a/integration-tests/src/integrationTest/resources/bulkimport/importhandler/office/Office.xls and /dev/null differ diff --git a/integration-tests/src/integrationTest/resources/bulkimport/importhandler/savings/Savings.xls b/integration-tests/src/integrationTest/resources/bulkimport/importhandler/savings/Savings.xls deleted file mode 100644 index ec42864d5..000000000 Binary files a/integration-tests/src/integrationTest/resources/bulkimport/importhandler/savings/Savings.xls and /dev/null differ
