You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ar...@apache.org on 2022/12/14 12:06:26 UTC
[fineract] branch develop updated: FINERACT-1744 - [x] Batch api support for idempotency - [x] BatchFilter for generic batch filtering - [x] LoanCOBApiFilter extend with BatchFilter support - [x] FineractRequestContextHolder generic context holder with batch and http request supports
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
The following commit(s) were added to refs/heads/develop by this push:
new 995b1ea02 FINERACT-1744 - [x] Batch api support for idempotency - [x] BatchFilter for generic batch filtering - [x] LoanCOBApiFilter extend with BatchFilter support - [x] FineractRequestContextHolder generic context holder with batch and http request supports
995b1ea02 is described below
commit 995b1ea021dd2d8c0d888853a8c5a199d31948a1
Author: Janos Haber <ja...@finesolution.hu>
AuthorDate: Fri Dec 9 00:16:21 2022 +0100
FINERACT-1744
- [x] Batch api support for idempotency
- [x] BatchFilter for generic batch filtering
- [x] LoanCOBApiFilter extend with BatchFilter support
- [x] FineractRequestContextHolder generic context holder with batch and http request supports
---
.../org/apache/fineract/batch/domain/Header.java | 2 +
.../batch/service/BatchApiServiceImpl.java | 101 +++++++++-------
.../commands/service/IdempotencyKeyResolver.java | 21 ++--
.../SynchronousCommandProcessingService.java | 17 ++-
.../core/domain/BatchRequestContextHolder.java | 64 ++++++++++
.../core/domain/FineractRequestContextHolder.java | 130 +++++++++++++++++++++
.../core/filters/BatchCallHandler.java | 46 ++++++++
.../core/filters/BatchFilter.java} | 27 ++---
.../core/filters/BatchFilterChain.java} | 25 +---
.../core/filters/IdempotencyStoreFilter.java | 63 ++++++++--
.../jobs/filter/LoanCOBApiFilter.java | 121 +++++++++++++++----
.../service/IdempotencyKeyResolverTest.java | 37 +++---
.../SynchronousCommandProcessingServiceTest.java | 5 +
.../BatchRequestsIntegrationTest.java | 93 +++++++++++++++
14 files changed, 600 insertions(+), 152 deletions(-)
diff --git a/fineract-provider/src/main/java/org/apache/fineract/batch/domain/Header.java b/fineract-provider/src/main/java/org/apache/fineract/batch/domain/Header.java
index 9e84625dd..3350d153e 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/batch/domain/Header.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/batch/domain/Header.java
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.batch.domain;
+import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@@ -32,6 +33,7 @@ import lombok.experimental.Accessors;
* @see BatchResponse
*/
@NoArgsConstructor
+@AllArgsConstructor
@Data
@Accessors(chain = true)
public class Header {
diff --git a/fineract-provider/src/main/java/org/apache/fineract/batch/service/BatchApiServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/batch/service/BatchApiServiceImpl.java
index 20502e3c1..663b250e6 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/batch/service/BatchApiServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/batch/service/BatchApiServiceImpl.java
@@ -22,7 +22,11 @@ import com.google.gson.Gson;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
@@ -33,15 +37,22 @@ import org.apache.fineract.batch.command.CommandStrategy;
import org.apache.fineract.batch.command.CommandStrategyProvider;
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.batch.exception.ClientDetailsNotFoundException;
import org.apache.fineract.batch.exception.ErrorHandler;
import org.apache.fineract.batch.exception.ErrorInfo;
import org.apache.fineract.batch.service.ResolutionHelper.BatchRequestNode;
+import org.apache.fineract.infrastructure.core.domain.BatchRequestContextHolder;
+import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
+import org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException;
+import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessFailedException;
+import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessSucceedException;
+import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessUnderProcessingException;
+import org.apache.fineract.infrastructure.core.filters.BatchCallHandler;
+import org.apache.fineract.infrastructure.core.filters.BatchFilter;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionException;
-import org.springframework.transaction.TransactionStatus;
-import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
/**
@@ -49,7 +60,6 @@ import org.springframework.transaction.support.TransactionTemplate;
* CommandStrategy from CommandStrategyProvider.
*
* @author Rishabh Shukla
- *
* @see org.apache.fineract.batch.domain.BatchRequest
* @see org.apache.fineract.batch.domain.BatchResponse
* @see org.apache.fineract.batch.command.CommandStrategyProvider
@@ -63,6 +73,10 @@ public class BatchApiServiceImpl implements BatchApiService {
private final ResolutionHelper resolutionHelper;
private final TransactionTemplate transactionTemplate;
+ private final List<BatchFilter> batchFilters;
+
+ private final FineractRequestContextHolder fineractRequestContextHolder;
+
/**
* Returns the response list by getting a proper {@link org.apache.fineract.batch.command.CommandStrategy}.
* execute() method of acquired commandStrategy is then provided with the separate Request.
@@ -84,6 +98,7 @@ public class BatchApiServiceImpl implements BatchApiService {
responseList.add(response);
return responseList;
}
+
for (BatchRequestNode rootNode : batchRequestNodes) {
final BatchRequest rootRequest = rootNode.getRequest();
final CommandStrategy commandStrategy = this.strategyProvider
@@ -100,9 +115,15 @@ public class BatchApiServiceImpl implements BatchApiService {
}
- private BatchResponse safelyExecuteStrategy(CommandStrategy commandStrategy, BatchRequest request, UriInfo uriInfo) {
+ private BatchResponse safelyExecuteStrategy(CommandStrategy commandStrategy, BatchRequest request, UriInfo originalUriInfo) {
try {
- return commandStrategy.execute(request, uriInfo);
+ BatchRequestContextHolder.setRequestAttributes(new HashMap<>(Optional.ofNullable(request.getHeaders())
+ .map(list -> list.stream().collect(Collectors.toMap(Header::getName, Header::getValue)))
+ .orElse(Collections.emptyMap())));
+
+ return new BatchCallHandler(this.batchFilters, commandStrategy::execute).serviceCall(request, originalUriInfo);
+ } catch (AbstractIdempotentCommandException idempotentException) {
+ return handleIdempotentRequests(idempotentException, request);
} catch (RuntimeException e) {
log.warn("Exception while executing batch strategy {}", commandStrategy.getClass().getSimpleName(), e);
@@ -114,13 +135,33 @@ public class BatchApiServiceImpl implements BatchApiService {
response.setStatusCode(ex.getStatusCode());
response.setBody(ex.getMessage());
return response;
+ } finally {
+ BatchRequestContextHolder.resetRequestAttributes();
+ }
+ }
+
+ private BatchResponse handleIdempotentRequests(AbstractIdempotentCommandException idempotentException, BatchRequest request) {
+ BatchResponse response = new BatchResponse();
+ response.setRequestId(request.getRequestId());
+ response.setHeaders(Optional.ofNullable(request.getHeaders()).orElse(new HashSet<>()));
+ response.getHeaders().add(new Header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER, "true"));
+ response.setBody(idempotentException.getResponse());
+ if (idempotentException instanceof IdempotentCommandProcessSucceedException) {
+ response.setStatusCode(200);
+ } else if (idempotentException instanceof IdempotentCommandProcessUnderProcessingException) {
+ response.setStatusCode(409);
+ } else if (idempotentException instanceof IdempotentCommandProcessFailedException) {
+ response.setStatusCode(((IdempotentCommandProcessFailedException) idempotentException).getStatusCode());
+ } else {
+ response.setStatusCode(500);
}
+ return response;
}
private List<BatchResponse> processChildRequests(final BatchRequestNode rootRequest, BatchResponse rootResponse, UriInfo uriInfo) {
final List<BatchResponse> childResponses = new ArrayList<>();
- if (rootRequest.getChildRequests().size() > 0) {
+ if (!rootRequest.getChildRequests().isEmpty()) {
for (BatchRequestNode childNode : rootRequest.getChildRequests()) {
@@ -175,30 +216,25 @@ public class BatchApiServiceImpl implements BatchApiService {
public List<BatchResponse> handleBatchRequestsWithEnclosingTransaction(final List<BatchRequest> requestList, final UriInfo uriInfo) {
List<BatchResponse> responseList = new ArrayList<>();
try {
- return this.transactionTemplate.execute(new TransactionCallback<List<BatchResponse>>() {
-
- @Override
- public List<BatchResponse> doInTransaction(TransactionStatus status) {
- try {
- responseList.addAll(handleBatchRequests(requestList, uriInfo));
- return responseList;
- } catch (RuntimeException ex) {
+ return this.transactionTemplate.execute(status -> {
+ try {
+ responseList.addAll(handleBatchRequests(requestList, uriInfo));
+ return responseList;
+ } catch (RuntimeException ex) {
- ErrorInfo e = ErrorHandler.handler(ex);
- BatchResponse errResponse = new BatchResponse();
- errResponse.setStatusCode(e.getStatusCode());
- errResponse.setBody(e.getMessage());
+ ErrorInfo e = ErrorHandler.handler(ex);
+ BatchResponse errResponse = new BatchResponse();
+ errResponse.setStatusCode(e.getStatusCode());
+ errResponse.setBody(e.getMessage());
- List<BatchResponse> errResponseList = new ArrayList<>();
- errResponseList.add(errResponse);
+ List<BatchResponse> errResponseList = new ArrayList<>();
+ errResponseList.add(errResponse);
- status.setRollbackOnly();
- return errResponseList;
- }
+ status.setRollbackOnly();
+ return errResponseList;
}
-
});
- } catch (TransactionException ex) {
+ } catch (TransactionException | NonTransientDataAccessException ex) {
ErrorInfo e = ErrorHandler.handler(ex);
BatchResponse errResponse = new BatchResponse();
errResponse.setStatusCode(e.getStatusCode());
@@ -213,21 +249,6 @@ public class BatchApiServiceImpl implements BatchApiService {
List<BatchResponse> errResponseList = new ArrayList<>();
errResponseList.add(errResponse);
- return errResponseList;
- } catch (final NonTransientDataAccessException ex) {
- ErrorInfo e = ErrorHandler.handler(ex);
- BatchResponse errResponse = new BatchResponse();
- errResponse.setStatusCode(e.getStatusCode());
-
- for (BatchResponse res : responseList) {
- if (!res.getStatusCode().equals(200)) {
- errResponse.setBody("Transaction is being rolled back. First erroneous request: \n" + new Gson().toJson(res));
- break;
- }
- }
- List<BatchResponse> errResponseList = new ArrayList<>();
- errResponseList.add(errResponse);
-
return errResponseList;
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/IdempotencyKeyResolver.java b/fineract-provider/src/main/java/org/apache/fineract/commands/service/IdempotencyKeyResolver.java
index 880aa0438..3787b83c5 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/commands/service/IdempotencyKeyResolver.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/commands/service/IdempotencyKeyResolver.java
@@ -21,29 +21,24 @@ package org.apache.fineract.commands.service;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandWrapper;
-import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
import org.springframework.stereotype.Component;
-import org.springframework.web.context.request.RequestContextHolder;
-import org.springframework.web.context.request.ServletRequestAttributes;
@Component
@RequiredArgsConstructor
public class IdempotencyKeyResolver {
- private final IdempotencyKeyGenerator idempotencyKeyGenerator;
+ private final FineractRequestContextHolder fineractRequestContextHolder;
- private final FineractProperties fineractProperties;
+ private final IdempotencyKeyGenerator idempotencyKeyGenerator;
public String resolve(CommandWrapper wrapper) {
- return Optional.ofNullable(wrapper.getIdempotencyKey())
- .orElseGet(() -> getHeaderAttribute().orElseGet(idempotencyKeyGenerator::create));
+ return Optional.ofNullable(wrapper.getIdempotencyKey()).orElseGet(() -> getAttribute().orElseGet(idempotencyKeyGenerator::create));
}
- private Optional<String> getHeaderAttribute() {
- return Optional.ofNullable(RequestContextHolder.getRequestAttributes()) //
- .filter(ServletRequestAttributes.class::isInstance) //
- .map(ServletRequestAttributes.class::cast) //
- .map(ServletRequestAttributes::getRequest) //
- .map(request -> request.getHeader(fineractProperties.getIdempotencyKeyHeaderName()));
+ private Optional<String> getAttribute() {
+ return Optional.ofNullable(fineractRequestContextHolder.getAttribute(SynchronousCommandProcessingService.IDEMPOTENCY_KEY_ATTRIBUTE))
+ .map(String::valueOf);
+
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java b/fineract-provider/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java
index 7216babe2..941d6869b 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java
@@ -29,7 +29,6 @@ import java.lang.reflect.Type;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
-import java.util.Optional;
import java.util.function.Function;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -45,6 +44,7 @@ import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDoma
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
+import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
import org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException;
import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessFailedException;
import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessSucceedException;
@@ -59,8 +59,6 @@ import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
-import org.springframework.web.context.request.RequestAttributes;
-import org.springframework.web.context.request.RequestContextHolder;
@Service
@Slf4j
@@ -68,6 +66,8 @@ import org.springframework.web.context.request.RequestContextHolder;
public class SynchronousCommandProcessingService implements CommandProcessingService {
public static final String IDEMPOTENCY_KEY_STORE_FLAG = "idempotencyKeyStoreFlag";
+
+ public static final String IDEMPOTENCY_KEY_ATTRIBUTE = "IdempotencyKeyAttribute";
public static final String COMMAND_SOURCE_ID = "commandSourceId";
private final PlatformSecurityContext context;
private final ApplicationContext applicationContext;
@@ -78,6 +78,8 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
private final IdempotencyKeyResolver idempotencyKeyResolver;
private final IdempotencyKeyGenerator idempotencyKeyGenerator;
private final CommandSourceService commandSourceService;
+
+ private final FineractRequestContextHolder fineractRequestContextHolder;
private final Gson gson = GoogleGsonSerializerHelper.createSimpleGson();
@Override
@@ -147,9 +149,8 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
saveCommandToRequest(savedCommandSource);
}
- private static void saveCommandToRequest(CommandSource savedCommandSource) {
- Optional.ofNullable(RequestContextHolder.getRequestAttributes()).ifPresent(requestAttributes -> requestAttributes
- .setAttribute(COMMAND_SOURCE_ID, savedCommandSource.getId(), RequestAttributes.SCOPE_REQUEST));
+ private void saveCommandToRequest(CommandSource savedCommandSource) {
+ fineractRequestContextHolder.setAttribute(COMMAND_SOURCE_ID, savedCommandSource.getId());
}
private void publishHookErrorEvent(CommandWrapper wrapper, JsonCommand command, Throwable t) {
@@ -176,9 +177,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
}
private void setIdempotencyKeyStoreFlag(boolean flag) {
- Optional.ofNullable(RequestContextHolder.getRequestAttributes()).ifPresent(
- requestAttributes -> requestAttributes.setAttribute(IDEMPOTENCY_KEY_STORE_FLAG, flag, RequestAttributes.SCOPE_REQUEST));
-
+ fineractRequestContextHolder.setAttribute(IDEMPOTENCY_KEY_STORE_FLAG, flag);
}
@Transactional
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/BatchRequestContextHolder.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/BatchRequestContextHolder.java
new file mode 100644
index 000000000..9cdbbf36a
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/BatchRequestContextHolder.java
@@ -0,0 +1,64 @@
+/**
+ * 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.domain;
+
+import java.util.Map;
+import org.springframework.core.NamedThreadLocal;
+
+public final class BatchRequestContextHolder {
+
+ private BatchRequestContextHolder() {}
+
+ private static final ThreadLocal<Map<String, Object>> batchAttributes = new NamedThreadLocal<>("BatchAttributesForProcessing");
+
+ /**
+ * True if the batch attributes are set
+ *
+ * @return true if the batch attributes are set
+ */
+ public static boolean isBatchRequest() {
+ return batchAttributes.get() != null;
+ }
+
+ /**
+ * Set the batch attributes for the current thread.
+ *
+ * @param requestAttributes
+ * the new batch attributes
+ */
+ public static void setRequestAttributes(Map<String, Object> requestAttributes) {
+ batchAttributes.set(requestAttributes);
+ }
+
+ /**
+ * Returns the batch attributes for the current thread.
+ *
+ * @return the batch attributes for the current thread, cna be null
+ */
+ public static Map<String, Object> getRequestAttributes() {
+ return batchAttributes.get();
+ }
+
+ /**
+ * Reset the batch attributes for the current thread.
+ */
+ public static void resetRequestAttributes() {
+ batchAttributes.remove();
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/FineractRequestContextHolder.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/FineractRequestContextHolder.java
new file mode 100644
index 000000000..7cb895416
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/FineractRequestContextHolder.java
@@ -0,0 +1,130 @@
+/**
+ * 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.domain;
+
+import java.util.Optional;
+import javax.servlet.http.HttpServletRequest;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+
+/**
+ * Common context holder to store environment-specific request attributes. <br/>
+ * It will try to get the attribute from the following order:
+ *
+ * <pre>
+ * <ul>
+ * <li>{@link HttpServletRequest} (if the parameter not null)</li>
+ * <li>{@link BatchRequestContextHolder} if it's a batch request set int the current thread</li>
+ * <li>{@link RequestContextHolder} if it exists in the current thread</li>
+ * </ul>
+ * </pre>
+ */
+@Component
+@NoArgsConstructor
+@Slf4j
+public final class FineractRequestContextHolder {
+
+ /**
+ * Get the attribute, the request not set using only thread bound context variables
+ *
+ * @param key
+ * attribute key
+ * @return attribute value or null if it not set in the current thread
+ */
+ public Object getAttribute(String key) {
+ return getAttribute(key, null);
+ }
+
+ /**
+ * Get the attribute.
+ *
+ * The method will check the request attribute first, then the thread bound context variables
+ *
+ * TODO: in {@link org.springframework.context.ApplicationEvent} always return null
+ *
+ * @param key
+ * attribute key
+ * @param request
+ * {@link HttpServletRequest} object
+ * @return attribute value or null if it not set in the current thread or {@link HttpServletRequest} (if not null)
+ */
+ public Object getAttribute(String key, HttpServletRequest request) {
+ if (request != null) {
+ return request.getAttribute(key);
+ } else if (isBatchRequest()) {
+ return Optional.ofNullable(BatchRequestContextHolder.getRequestAttributes()).map(attributes -> attributes.get(key))
+ .orElse(null);
+ } else if (RequestContextHolder.getRequestAttributes() != null) {
+ return Optional.ofNullable(RequestContextHolder.getRequestAttributes())
+ .map(r -> r.getAttribute(key, RequestAttributes.SCOPE_REQUEST)).orElse(null);
+ }
+ return null;
+ }
+
+ /**
+ * Set the attribute
+ *
+ * @param key
+ * attribute key
+ * @param value
+ * attribute value
+ */
+ public void setAttribute(String key, Object value) {
+ setAttribute(key, value, null);
+ }
+
+ /**
+ * Set the attribute.
+ *
+ * If the request is not null, it will set the attribute in the request otherwise it will set it in the thread bound
+ * context variables
+ *
+ * TODO: in {@link org.springframework.context.ApplicationEvent} the attributes not set
+ *
+ * @param key
+ * attribute key
+ * @param value
+ * attribute value
+ * @param request
+ * {@link HttpServletRequest} object
+ */
+ public void setAttribute(String key, Object value, HttpServletRequest request) {
+ if (request != null) {
+ request.setAttribute(key, value);
+ } else if (isBatchRequest()) {
+ Optional.ofNullable(BatchRequestContextHolder.getRequestAttributes()).ifPresent(attributes -> attributes.put(key, value));
+ } else if (RequestContextHolder.getRequestAttributes() != null) {
+ Optional.ofNullable(RequestContextHolder.getRequestAttributes())
+ .ifPresent(requestAttributes -> requestAttributes.setAttribute(key, value, RequestAttributes.SCOPE_REQUEST));
+ }
+
+ }
+
+ /**
+ * True if {@link BatchRequestContextHolder} will be set in the current thread
+ *
+ * @return true if the current request is a batch request
+ */
+ public static boolean isBatchRequest() {
+ return BatchRequestContextHolder.isBatchRequest();
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/BatchCallHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/BatchCallHandler.java
new file mode 100644
index 000000000..11415ef05
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/BatchCallHandler.java
@@ -0,0 +1,46 @@
+/**
+ * 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 java.util.List;
+import java.util.function.BiFunction;
+import javax.ws.rs.core.UriInfo;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.batch.domain.BatchRequest;
+import org.apache.fineract.batch.domain.BatchResponse;
+
+@RequiredArgsConstructor
+public class BatchCallHandler implements BatchFilterChain {
+
+ private final List<? extends BatchFilter> filters;
+
+ private final BiFunction<BatchRequest, UriInfo, BatchResponse> lastElement;
+ private int currentPosition = 0;
+
+ @Override
+ public BatchResponse serviceCall(BatchRequest batchRequest, UriInfo uriInfo) {
+ if (this.currentPosition == filters.size()) {
+ return lastElement.apply(batchRequest, uriInfo);
+ } else {
+ BatchFilter currentFilter = filters.get(this.currentPosition);
+ this.currentPosition++;
+ return currentFilter.doFilter(batchRequest, uriInfo, this);
+ }
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/batch/domain/Header.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/BatchFilter.java
similarity index 58%
copy from fineract-provider/src/main/java/org/apache/fineract/batch/domain/Header.java
copy to fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/BatchFilter.java
index 9e84625dd..21505fe96 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/batch/domain/Header.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/BatchFilter.java
@@ -16,26 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.batch.domain;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-import lombok.experimental.Accessors;
+package org.apache.fineract.infrastructure.core.filters;
-/**
- * Provides an object to handle HTTP headers as name and value pairs for Batch API. It is used in {@link BatchRequest}
- * and {@link BatchResponse} to store the information regarding the headers in incoming and outgoing JSON Strings.
- *
- * @author Rishabh Shukla
- *
- * @see BatchRequest
- * @see BatchResponse
- */
-@NoArgsConstructor
-@Data
-@Accessors(chain = true)
-public class Header {
+import javax.ws.rs.core.UriInfo;
+import org.apache.fineract.batch.domain.BatchRequest;
+import org.apache.fineract.batch.domain.BatchResponse;
+
+public interface BatchFilter {
+
+ BatchResponse doFilter(BatchRequest batchRequest, UriInfo uriInfo, BatchFilterChain chain);
- private String name;
- private String value;
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/batch/domain/Header.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/BatchFilterChain.java
similarity index 58%
copy from fineract-provider/src/main/java/org/apache/fineract/batch/domain/Header.java
copy to fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/BatchFilterChain.java
index 9e84625dd..484dbbebd 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/batch/domain/Header.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/BatchFilterChain.java
@@ -16,26 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.batch.domain;
+package org.apache.fineract.infrastructure.core.filters;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-import lombok.experimental.Accessors;
+import javax.ws.rs.core.UriInfo;
+import org.apache.fineract.batch.domain.BatchRequest;
+import org.apache.fineract.batch.domain.BatchResponse;
-/**
- * Provides an object to handle HTTP headers as name and value pairs for Batch API. It is used in {@link BatchRequest}
- * and {@link BatchResponse} to store the information regarding the headers in incoming and outgoing JSON Strings.
- *
- * @author Rishabh Shukla
- *
- * @see BatchRequest
- * @see BatchResponse
- */
-@NoArgsConstructor
-@Data
-@Accessors(chain = true)
-public class Header {
+public interface BatchFilterChain {
- private String name;
- private String value;
+ BatchResponse serviceCall(BatchRequest batchRequest, UriInfo uriInfo);
}
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 9a9a19c12..2030f2de0 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
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.infrastructure.core.filters;
+import io.vavr.collection.List;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
@@ -25,13 +26,19 @@ import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.core.UriInfo;
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;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@@ -40,11 +47,15 @@ import org.springframework.web.util.ContentCachingResponseWrapper;
@RequiredArgsConstructor
@Slf4j
@Component
-public class IdempotencyStoreFilter extends OncePerRequestFilter {
+public class IdempotencyStoreFilter extends OncePerRequestFilter implements BatchFilter {
private final CommandSourceRepository commandSourceRepository;
private final CommandSourceService commandSourceService;
+ private final FineractProperties fineractProperties;
+
+ private final FineractRequestContextHolder fineractRequestContextHolder;
+
@Override
protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,
@NotNull FilterChain filterChain) throws ServletException, IOException {
@@ -52,23 +63,34 @@ public class IdempotencyStoreFilter extends OncePerRequestFilter {
if (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)
&& wrapper.getValue() != null;
if (isSuccessWithoutStored) {
- commandSourceRepository.findById(commandId.get()).ifPresent(commandSource -> {
- commandSource.setResultStatusCode(response.getStatus());
- commandSource.setResult(new String(wrapper.getValue().getContentAsByteArray(), StandardCharsets.UTF_8));
- commandSourceService.saveResult(commandSource);
- });
+ storeCommandResult(response.getStatus(), new String(wrapper.getValue().getContentAsByteArray(), StandardCharsets.UTF_8),
+ commandId);
}
if (wrapper.getValue() != null) {
wrapper.getValue().copyBodyToResponse();
}
}
+ private void storeCommandResult(int response, String body, Optional<Long> commandId) {
+ commandSourceRepository.findById(commandId.get()).ifPresent(commandSource -> {
+ commandSource.setResultStatusCode(response);
+ commandSource.setResult(body);
+ 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);
@@ -80,12 +102,37 @@ public class IdempotencyStoreFilter extends OncePerRequestFilter {
}
private boolean isStoreIdempotencyKey(HttpServletRequest request) {
- return Optional.ofNullable(request.getAttribute(SynchronousCommandProcessingService.IDEMPOTENCY_KEY_STORE_FLAG))
+ 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(request.getAttribute(SynchronousCommandProcessingService.COMMAND_SOURCE_ID))
+ return Optional
+ .ofNullable(fineractRequestContextHolder.getAttribute(SynchronousCommandProcessingService.COMMAND_SOURCE_ID, request))
.filter(Long.class::isInstance).map(Long.class::cast);
}
+
+ private Optional<String> extractIdempotentKeyFromBatchRequest(BatchRequest request) {
+ return Optional.ofNullable(request.getHeaders()) //
+ .map(List::ofAll) //
+ .flatMap(headers -> headers.find(header -> header.getName().equals(fineractProperties.getIdempotencyKeyHeaderName()))
+ .toJavaOptional()) //
+ .map(Header::getValue); //
+
+ }
+
+ @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(result.getStatusCode(), result.getBody(), commandId);
+ }
+ return result;
+ }
}
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 f3a3a2488..f31ef3127 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
@@ -19,6 +19,7 @@
package org.apache.fineract.infrastructure.jobs.filter;
import com.google.common.base.Splitter;
+import io.vavr.control.Either;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Collections;
@@ -32,11 +33,16 @@ import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.core.UriInfo;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
+import org.apache.fineract.batch.domain.BatchRequest;
+import org.apache.fineract.batch.domain.BatchResponse;
import org.apache.fineract.cob.service.InlineLoanCOBExecutorServiceImpl;
import org.apache.fineract.cob.service.LoanAccountLockService;
import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
+import org.apache.fineract.infrastructure.core.filters.BatchFilter;
+import org.apache.fineract.infrastructure.core.filters.BatchFilterChain;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository;
import org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonitoringAccount;
@@ -49,7 +55,7 @@ import org.springframework.web.filter.OncePerRequestFilter;
@Component
@RequiredArgsConstructor
-public class LoanCOBApiFilter extends OncePerRequestFilter {
+public class LoanCOBApiFilter extends OncePerRequestFilter implements BatchFilter {
private final GLIMAccountInfoRepository glimAccountInfoRepository;
private final LoanAccountLockService loanAccountLockService;
@@ -68,39 +74,83 @@ public class LoanCOBApiFilter extends OncePerRequestFilter {
private static final Integer GLIM_STRING_INDEX_IN_URL = 2;
private static final String JOB_NAME = "INLINE_LOAN_COB";
+ private static class Reject {
+
+ private final String message;
+ private final Integer statusCode;
+
+ Reject(String message, Integer statusCode) {
+ this.message = message;
+ this.statusCode = statusCode;
+ }
+
+ public static Reject reject(Long loanId, int status) {
+ return new Reject(ApiGlobalErrorResponse.loanIsLocked(loanId).toJson(), status);
+ }
+
+ public void toServletResponse(HttpServletResponse response) throws IOException {
+ response.setStatus(statusCode);
+ response.getWriter().write(message);
+ }
+
+ public BatchResponse toBatchResponse(BatchRequest request) {
+ BatchResponse response = new BatchResponse();
+ response.setStatusCode(statusCode);
+ response.setBody(message);
+ response.setHeaders(request.getHeaders());
+ response.setRequestId(request.getRequestId());
+ return response;
+ }
+ }
+
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
- if (!isOnApiList(request) || isBypassUser(response)) {
+ if (!isOnApiList(request.getPathInfo(), request.getMethod())) {
proceed(filterChain, request, response);
} else {
- Iterable<String> split = Splitter.on('/').split(request.getPathInfo());
- Supplier<Stream<String>> streamSupplier = () -> StreamSupport.stream(split.spliterator(), false);
- boolean isGlim = isGlim(streamSupplier);
- Long loanIdFromRequest = getLoanId(isGlim, streamSupplier);
- List<Long> loanIds = isGlim ? getGlimChildLoanIds(loanIdFromRequest) : Collections.singletonList(loanIdFromRequest);
- if (isLoanHardLocked(loanIds)) {
- reject(loanIdFromRequest, response, HttpStatus.SC_CONFLICT);
- } else if (isLoanSoftLocked(loanIds)) {
- executeInlineCob(loanIds);
+ Either<Reject, Boolean> bypassUser = isBypassUser();
+ if (bypassUser.isRight() && Boolean.TRUE.equals(bypassUser.get())) {
proceed(filterChain, request, response);
+ } else if (bypassUser.isLeft()) {
+ bypassUser.getLeft().toServletResponse(response);
} else {
- proceed(filterChain, request, response);
+ Either<Reject, List<Long>> result = loanIdCalculation(request.getPathInfo());
+ if (result.isLeft()) {
+ result.getLeft().toServletResponse(response);
+ } else {
+ if (isLoanSoftLocked(result.get())) {
+ executeInlineCob(result.get());
+ }
+ proceed(filterChain, request, response);
+ }
}
}
}
+ private Either<Reject, List<Long>> loanIdCalculation(String pathInfo) {
+ Iterable<String> split = Splitter.on('/').split(pathInfo);
+ Supplier<Stream<String>> streamSupplier = () -> StreamSupport.stream(split.spliterator(), false);
+ boolean isGlim = isGlim(streamSupplier);
+ Long loanIdFromRequest = getLoanId(isGlim, streamSupplier);
+ List<Long> loanIds = isGlim ? getGlimChildLoanIds(loanIdFromRequest) : Collections.singletonList(loanIdFromRequest);
+ if (isLoanHardLocked(loanIds)) {
+ return Either.left(Reject.reject(loanIdFromRequest, HttpStatus.SC_CONFLICT));
+ } else {
+ return Either.right(loanIds);
+ }
+ }
+
private void executeInlineCob(List<Long> loanIds) {
inlineLoanCOBExecutorService.execute(loanIds, JOB_NAME);
}
- private boolean isBypassUser(HttpServletResponse response) throws IOException {
+ private Either<Reject, Boolean> isBypassUser() {
try {
- return context.authenticatedUser().isBypassUser();
+ return Either.right(context.authenticatedUser().isBypassUser());
} catch (UnAuthenticatedUserException e) {
- reject(null, response, HttpStatus.SC_UNAUTHORIZED);
+ return Either.left(Reject.reject(null, HttpStatus.SC_UNAUTHORIZED));
}
- return false;
}
private List<Long> getGlimChildLoanIds(Long loanIdFromRequest) {
@@ -131,12 +181,6 @@ public class LoanCOBApiFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response);
}
- private void reject(Long loanId, HttpServletResponse response, int status) throws IOException {
- response.setStatus(status);
- ApiGlobalErrorResponse errorResponse = ApiGlobalErrorResponse.loanIsLocked(loanId);
- response.getWriter().write(errorResponse.toJson());
- }
-
private Long getLoanId(boolean isGlim, Supplier<Stream<String>> streamSupplier) {
if (!isGlim) {
return streamSupplier.get().skip(LOAN_ID_INDEX_IN_URL).findFirst().map(Long::valueOf).orElse(null);
@@ -145,14 +189,41 @@ public class LoanCOBApiFilter extends OncePerRequestFilter {
}
}
- private boolean isOnApiList(HttpServletRequest request) {
- if (StringUtils.isBlank(request.getPathInfo())) {
+ private boolean isOnApiList(String pathInfo, String method) {
+ if (StringUtils.isBlank(pathInfo)) {
return false;
}
- return HTTP_METHODS.contains(HttpMethod.valueOf(request.getMethod())) && URL_FUNCTION.test(request.getPathInfo());
+ return HTTP_METHODS.contains(HttpMethod.valueOf(method)) && URL_FUNCTION.test(pathInfo);
}
private boolean isGlim(Supplier<Stream<String>> streamSupplier) {
return streamSupplier.get().skip(GLIM_STRING_INDEX_IN_URL).findFirst().map(s -> s.equals("glimAccount")).orElse(false);
}
+
+ @Override
+ public BatchResponse doFilter(BatchRequest batchRequest, UriInfo uriInfo, BatchFilterChain chain) {
+ if (!isOnApiList("/" + batchRequest.getRelativeUrl(), batchRequest.getMethod())) {
+ return chain.serviceCall(batchRequest, uriInfo);
+ } else {
+ Either<Reject, Boolean> bypassUser = isBypassUser();
+ if (bypassUser.isRight() && Boolean.TRUE.equals(bypassUser.get())) {
+ return chain.serviceCall(batchRequest, uriInfo);
+ } else if (bypassUser.isLeft()) {
+ return bypassUser.getLeft().toBatchResponse(batchRequest);
+ } else {
+ Either<Reject, List<Long>> result = loanIdCalculation("/" + batchRequest.getRelativeUrl());
+ if (result.isLeft()) {
+ return result.getLeft().toBatchResponse(batchRequest);
+ } else {
+ if (!isLoanSoftLocked(result.get())) {
+ executeInlineCob(result.get());
+ }
+ return chain.serviceCall(batchRequest, uriInfo);
+
+ }
+ }
+ }
+
+ }
+
}
diff --git a/fineract-provider/src/test/java/org/apache/fineract/commands/service/IdempotencyKeyResolverTest.java b/fineract-provider/src/test/java/org/apache/fineract/commands/service/IdempotencyKeyResolverTest.java
index cc9acc7e3..6e6b0af85 100644
--- a/fineract-provider/src/test/java/org/apache/fineract/commands/service/IdempotencyKeyResolverTest.java
+++ b/fineract-provider/src/test/java/org/apache/fineract/commands/service/IdempotencyKeyResolverTest.java
@@ -18,46 +18,47 @@
*/
package org.apache.fineract.commands.service;
+import static org.mockito.Mockito.when;
+
+import java.util.HashMap;
import org.apache.fineract.commands.domain.CommandWrapper;
-import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.apache.fineract.infrastructure.core.domain.BatchRequestContextHolder;
+import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
-import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
-import org.springframework.mock.web.MockHttpServletRequest;
-import org.springframework.web.context.request.RequestAttributes;
-import org.springframework.web.context.request.RequestContextHolder;
-import org.springframework.web.context.request.ServletRequestAttributes;
+import org.mockito.Spy;
public class IdempotencyKeyResolverTest {
@Mock
private IdempotencyKeyGenerator idempotencyKeyGenerator;
- @Mock
- private FineractProperties fineractProperties;
-
@InjectMocks
private IdempotencyKeyResolver underTest;
+ @Spy
+ private FineractRequestContextHolder fineractRequestContextHolder;
+
@BeforeEach
public void setup() {
MockitoAnnotations.openMocks(this);
+ BatchRequestContextHolder.setRequestAttributes(new HashMap<>());
+ }
+
+ @AfterEach
+ public void tearDown() {
+ BatchRequestContextHolder.resetRequestAttributes();
}
@Test
public void testIPKResolveFromRequest() {
- String idkh = "foo";
String idk = "bar";
- Mockito.when(fineractProperties.getIdempotencyKeyHeaderName()).thenReturn(idkh);
-
- MockHttpServletRequest request = new MockHttpServletRequest();
- request.addHeader(idkh, idk);
- RequestAttributes requestAttributes = new ServletRequestAttributes(request);
- RequestContextHolder.setRequestAttributes(requestAttributes);
+ fineractRequestContextHolder.setAttribute(SynchronousCommandProcessingService.IDEMPOTENCY_KEY_ATTRIBUTE, idk);
CommandWrapper wrapper = CommandWrapper.wrap("act", "ent", 1L, 1L);
String resolvedIdk = underTest.resolve(wrapper);
Assertions.assertEquals(idk, resolvedIdk);
@@ -66,8 +67,7 @@ public class IdempotencyKeyResolverTest {
@Test
public void testIPKResolveFromGenerate() {
String idk = "idk";
- Mockito.when(idempotencyKeyGenerator.create()).thenReturn(idk);
- RequestContextHolder.setRequestAttributes(null);
+ when(idempotencyKeyGenerator.create()).thenReturn(idk);
CommandWrapper wrapper = CommandWrapper.wrap("act", "ent", 1L, 1L);
String resolvedIdk = underTest.resolve(wrapper);
Assertions.assertEquals(idk, resolvedIdk);
@@ -75,7 +75,6 @@ public class IdempotencyKeyResolverTest {
@Test
public void testIPKResolveFromWrapper() {
- RequestContextHolder.setRequestAttributes(null);
String idk = "idk";
CommandWrapper wrapper = new CommandWrapper(null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, idk);
diff --git a/fineract-provider/src/test/java/org/apache/fineract/commands/service/SynchronousCommandProcessingServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/commands/service/SynchronousCommandProcessingServiceTest.java
index 66a00ec9e..2840a4f15 100644
--- a/fineract-provider/src/test/java/org/apache/fineract/commands/service/SynchronousCommandProcessingServiceTest.java
+++ b/fineract-provider/src/test/java/org/apache/fineract/commands/service/SynchronousCommandProcessingServiceTest.java
@@ -31,6 +31,7 @@ import org.apache.fineract.commands.provider.CommandHandlerProvider;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.useradministration.domain.AppUser;
@@ -41,6 +42,7 @@ import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@@ -66,6 +68,9 @@ public class SynchronousCommandProcessingServiceTest {
@Mock
private CommandSourceService commandSourceService;
+ @Spy
+ private FineractRequestContextHolder fineractRequestContextHolder;
+
@InjectMocks
private SynchronousCommandProcessingService underTest;
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BatchRequestsIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BatchRequestsIntegrationTest.java
index b7d248c9b..694de4e12 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BatchRequestsIntegrationTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BatchRequestsIntegrationTest.java
@@ -25,9 +25,13 @@ import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import java.security.SecureRandom;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
+import java.util.UUID;
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.infrastructure.core.exception.AbstractIdempotentCommandException;
import org.apache.fineract.integrationtests.common.BatchHelper;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.CollateralManagementHelper;
@@ -146,4 +150,93 @@ public class BatchRequestsIntegrationTest {
Assertions.assertEquals(200L, (long) res.getStatusCode(), "Verify Status Code 200");
}
}
+
+ @Test
+ public void shouldReturnOkStatusWithIdempotencySupport() {
+
+ // Generate a random count of number of clients to be created
+ final Integer clientsCount = (int) Math.ceil(secureRandom.nextDouble() * 7) + 3;
+ final Integer[] clientIDs = new Integer[clientsCount];
+
+ // Create a new group and get its groupId
+ Integer groupID = GroupHelper.createGroup(this.requestSpec, this.responseSpec, true);
+
+ // Create new clients and add those to this group
+ for (Integer i = 0; i < clientsCount; i++) {
+ clientIDs[i] = ClientHelper.createClient(this.requestSpec, this.responseSpec);
+ groupID = GroupHelper.associateClient(this.requestSpec, this.responseSpec, groupID.toString(), clientIDs[i].toString());
+ LOG.info("client {} has been added to the group {}", clientIDs[i], groupID);
+ }
+
+ // Generate a random count of number of new loan products to be created
+ final Integer loansCount = (int) Math.ceil(secureRandom.nextDouble() * 4) + 1;
+ final Integer[] loanProducts = new Integer[loansCount];
+
+ // Create new loan Products
+ LoanTransactionHelper helper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
+ for (Integer i = 0; i < loansCount; i++) {
+ final String loanProductJSON = new LoanProductTestBuilder() //
+ .withPrincipal(String.valueOf(10000.00 + Math.ceil(secureRandom.nextDouble() * 1000000.00))) //
+ .withNumberOfRepayments(String.valueOf(2 + (int) Math.ceil(secureRandom.nextDouble() * 36))) //
+ .withRepaymentAfterEvery(String.valueOf(1 + (int) Math.ceil(secureRandom.nextDouble() * 3))) //
+ .withRepaymentTypeAsMonth() //
+ .withinterestRatePerPeriod(String.valueOf(1 + (int) Math.ceil(secureRandom.nextDouble() * 4))) //
+ .withInterestRateFrequencyTypeAsMonths() //
+ .withAmortizationTypeAsEqualPrincipalPayment() //
+ .withInterestTypeAsDecliningBalance() //
+ .currencyDetails("0", "100").build(null);
+
+ loanProducts[i] = helper.getLoanProductId(loanProductJSON);
+ }
+
+ // Select anyone of the loan products at random
+ final Integer loanProductID = loanProducts[(int) Math.floor(secureRandom.nextDouble() * (loansCount - 1))];
+
+ final List<BatchRequest> batchRequests = new ArrayList<>();
+
+ // Select a few clients from created group at random
+ Integer selClientsCount = (int) Math.ceil(secureRandom.nextDouble() * clientsCount) + 2;
+ for (int i = 0; i < selClientsCount; i++) {
+
+ final Integer collateralId = CollateralManagementHelper.createCollateralProduct(this.requestSpec, this.responseSpec);
+ Assertions.assertNotNull(collateralId);
+ final Integer clientCollateralId = CollateralManagementHelper.createClientCollateral(this.requestSpec, this.responseSpec,
+ String.valueOf(clientIDs[(int) Math.floor(secureRandom.nextDouble() * (clientsCount - 1))]), collateralId);
+ Assertions.assertNotNull(clientCollateralId);
+
+ BatchRequest br = BatchHelper.applyLoanRequest((long) selClientsCount, null, loanProductID, clientCollateralId);
+ br.setBody(br.getBody().replace("$.clientId",
+ String.valueOf(clientIDs[(int) Math.floor(secureRandom.nextDouble() * (clientsCount - 1))])));
+ br.setHeaders(new HashSet<>());
+ br.getHeaders().add(new Header("Idempotency-Key", UUID.randomUUID().toString()));
+ batchRequests.add(br);
+ }
+
+ // Send the request to Batch - API
+ final String jsonifiedRequest = BatchHelper.toJsonString(batchRequests);
+
+ final List<BatchResponse> response = BatchHelper.postBatchRequestsWithoutEnclosingTransaction(this.requestSpec, this.responseSpec,
+ jsonifiedRequest);
+
+ // Verify that each loan has been applied successfully
+ for (BatchResponse res : response) {
+ Assertions.assertFalse(io.vavr.collection.List.ofAll(res.getHeaders())
+ .find(header -> header.getName().equals(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER)).isDefined(),
+ "First can not be cached!");
+ Assertions.assertEquals(200L, (long) res.getStatusCode(), "Verify Status Code 200");
+ }
+
+ final List<BatchResponse> secondResponse = BatchHelper.postBatchRequestsWithoutEnclosingTransaction(this.requestSpec,
+ this.responseSpec, jsonifiedRequest);
+
+ // Verify that each loan has been applied successfully
+ for (BatchResponse res : secondResponse) {
+ Assertions.assertEquals("true",
+ io.vavr.collection.List.ofAll(res.getHeaders())
+ .find(header -> header.getName().equals(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER))
+ .map(Header::getValue).get(),
+ "Not cached by idempotency key!");
+ Assertions.assertEquals(200L, (long) res.getStatusCode(), "Verify Status Code 200");
+ }
+ }
}