FINERACT-1971: Handling inline COB execution before batch API execution outside of the batch transaction

This commit is contained in:
Arnold Galovics
2024-02-06 16:00:17 +01:00
committed by Arnold Gálovics
parent 99352c66b2
commit 770719e8ec
11 changed files with 484 additions and 121 deletions
@@ -0,0 +1,88 @@
/**
* 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.http;
import static java.nio.charset.StandardCharsets.UTF_8;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.apache.commons.io.IOUtils;
public class BodyCachingHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final byte[] cachedBody;
@SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW")
public BodyCachingHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
this.cachedBody = IOUtils.toByteArray(request.getInputStream());
}
@Override
public ServletInputStream getInputStream() throws IOException {
return new CachedBodyServletInputStream(cachedBody);
}
@Override
public BufferedReader getReader() throws IOException {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cachedBody);
return new BufferedReader(new InputStreamReader(byteArrayInputStream, UTF_8));
}
public static class CachedBodyServletInputStream extends ServletInputStream {
private final InputStream inputStream;
public CachedBodyServletInputStream(byte[] cachedBody) {
this.inputStream = new ByteArrayInputStream(cachedBody);
}
@Override
public boolean isFinished() {
try {
return inputStream.available() == 0;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
throw new UnsupportedOperationException();
}
@Override
public int read() throws IOException {
return inputStream.read();
}
}
}
@@ -27,6 +27,7 @@ import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.cob.conditions.LoanCOBEnabledCondition;
import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
import org.apache.fineract.infrastructure.core.http.BodyCachingHttpServletRequestWrapper;
import org.apache.fineract.infrastructure.jobs.exception.LoanIdsHardLockedException;
import org.apache.fineract.useradministration.exception.UnAuthenticatedUserException;
import org.apache.http.HttpStatus;
@@ -62,7 +63,9 @@ public class LoanCOBApiFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!helper.isOnApiList(request.getPathInfo(), request.getMethod())) {
request = new BodyCachingHttpServletRequestWrapper(request);
if (!helper.isOnApiList(request)) {
proceed(filterChain, request, response);
} else {
try {
@@ -71,7 +74,7 @@ public class LoanCOBApiFilter extends OncePerRequestFilter {
proceed(filterChain, request, response);
} else {
try {
List<Long> loanIds = helper.calculateRelevantLoanIds(request.getPathInfo());
List<Long> loanIds = helper.calculateRelevantLoanIds(request);
if (!loanIds.isEmpty() && helper.isLoanBehind(loanIds)) {
helper.executeInlineCob(loanIds);
}
@@ -1,74 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.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.cob.conditions.LoanCOBEnabledCondition;
import org.apache.fineract.infrastructure.core.filters.BatchRequestPreprocessor;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
@Component
@RequiredArgsConstructor
@Conditional(LoanCOBEnabledCondition.class)
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);
});
}
}
@@ -18,7 +18,15 @@
*/
package org.apache.fineract.infrastructure.jobs.filter;
import static org.apache.fineract.batch.command.CommandStrategyUtils.isRelativeUrlVersioned;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
@@ -28,6 +36,7 @@ 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.conditions.LoanCOBEnabledCondition;
import org.apache.fineract.cob.data.LoanIdAndLastClosedBusinessDate;
import org.apache.fineract.cob.loan.RetrieveLoanIdService;
@@ -62,6 +71,7 @@ public class LoanCOBFilterHelper {
private final RetrieveLoanIdService retrieveLoanIdService;
private final LoanRescheduleRequestRepository loanRescheduleRequestRepository;
private final ObjectMapper objectMapper;
private static final List<HttpMethod> HTTP_METHODS = List.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE);
@@ -100,14 +110,51 @@ public class LoanCOBFilterHelper {
return LOAN_PATH_PATTERN.matcher(pathInfo).matches() && pathInfo.contains("/v1/rescheduleloans/");
}
public boolean isOnApiList(String pathInfo, String method) {
public boolean isOnApiList(HttpServletRequest request) throws IOException {
String pathInfo = request.getPathInfo();
String method = request.getMethod();
if (StringUtils.isBlank(pathInfo)) {
return false;
}
if (isBatchApi(pathInfo)) {
return isBatchApiMatching(request);
} else {
return isApiMatching(method, pathInfo);
}
}
private boolean isBatchApiMatching(HttpServletRequest request) throws IOException {
for (BatchRequest batchRequest : getBatchRequests(request)) {
String method = batchRequest.getMethod();
String pathInfo = batchRequest.getRelativeUrl();
if (isApiMatching(method, pathInfo)) {
return true;
}
}
return false;
}
private List<BatchRequest> getBatchRequests(HttpServletRequest request) throws IOException {
List<BatchRequest> batchRequests = objectMapper.readValue(request.getInputStream(), new TypeReference<>() {});
for (BatchRequest batchRequest : batchRequests) {
String pathInfo = "/" + batchRequest.getRelativeUrl();
if (!isRelativeUrlVersioned(batchRequest.getRelativeUrl())) {
pathInfo = "/v1/" + batchRequest.getRelativeUrl();
}
batchRequest.setRelativeUrl(pathInfo);
}
return batchRequests;
}
private boolean isApiMatching(String method, String pathInfo) {
return HTTP_METHODS.contains(HttpMethod.valueOf(method)) && !IGNORE_LOAN_PATH_PATTERN.matcher(pathInfo).find()
&& URL_FUNCTION.test(pathInfo);
}
private boolean isBatchApi(String pathInfo) {
return pathInfo.startsWith("/v1/batches");
}
private boolean isGlim(String pathInfo) {
return LOAN_GLIMACCOUNT_PATH_PATTERN.matcher(pathInfo).matches();
}
@@ -138,8 +185,47 @@ public class LoanCOBFilterHelper {
return CollectionUtils.isNotEmpty(loanIdAndLastClosedBusinessDates);
}
public List<Long> calculateRelevantLoanIds(String pathInfo) {
public List<Long> calculateRelevantLoanIds(HttpServletRequest request) throws IOException {
String pathInfo = request.getPathInfo();
if (isBatchApi(pathInfo)) {
return getLoanIdsFromBatchApi(request);
} else {
return getLoanIdsFromApi(pathInfo);
}
}
private List<Long> getLoanIdsFromBatchApi(HttpServletRequest request) throws IOException {
List<Long> loanIds = new ArrayList<>();
for (BatchRequest batchRequest : getBatchRequests(request)) {
// check the URL for Loan related ID
String relativeUrl = batchRequest.getRelativeUrl();
if (!relativeUrl.contains("$.resourceId")) {
// if resourceId reference is used, we simply don't know the resourceId without executing the requests
// first, so skipping it
loanIds.addAll(getLoanIdsFromApi(relativeUrl));
}
// check the body for Loan ID
Long loanId = getTopLevelLoanIdFromBatchRequest(batchRequest);
if (loanId != null) {
loanIds.add(loanId);
}
}
return loanIds;
}
private Long getTopLevelLoanIdFromBatchRequest(BatchRequest batchRequest) throws JsonProcessingException {
String body = batchRequest.getBody();
if (StringUtils.isNotBlank(body)) {
JsonNode jsonNode = objectMapper.readTree(body);
if (jsonNode.has("loanId")) {
return jsonNode.get("loanId").asLong();
}
}
return null;
}
private List<Long> getLoanIdsFromApi(String pathInfo) {
List<Long> loanIds = getLoanIdList(pathInfo);
if (isLoanHardLocked(loanIds)) {
throw new LoanIdsHardLockedException(loanIds.get(0));