FINERACT-1927: Incorrect exception info in BatchAPI response

This commit is contained in:
jmarta
2023-09-27 19:13:26 +02:00
committed by Adam Saghy
parent f452ce2cad
commit 32da7db35d
55 changed files with 1399 additions and 1134 deletions
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.batch.command;
import static jakarta.ws.rs.HttpMethod.DELETE;
import static jakarta.ws.rs.HttpMethod.GET;
import static jakarta.ws.rs.HttpMethod.POST;
import static jakarta.ws.rs.HttpMethod.PUT;
@@ -207,7 +208,9 @@ public class CommandStrategyProvider {
.method(PUT).build(), "updateDatatableEntryOneToOneCommandStrategy");
commandStrategies.put(CommandContext
.resource("v1\\/datatables\\/" + ALPHANUMBERIC_WITH_UNDERSCORE_REGEX + "\\/" + NUMBER_REGEX + "\\/" + NUMBER_REGEX)
.method(PUT).build(), "updateDatatableEntryOneToManyCommandStrategy");
.method(DELETE).build(), "deleteDatatableEntryOneToManyCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/datatables\\/" + ALPHANUMBERIC_WITH_UNDERSCORE_REGEX + "\\/" + NUMBER_REGEX)
.method(DELETE).build(), "deleteDatatableEntryOneToOneCommandStrategy");
commandStrategies.put(CommandContext
.resource("v1\\/datatables\\/" + ALPHANUMBERIC_WITH_UNDERSCORE_REGEX + "\\/" + NUMBER_REGEX + OPTIONAL_QUERY_PARAM_REGEX)
.method(GET).build(), "getDatatableEntryByAppTableIdCommandStrategy");
@@ -20,10 +20,13 @@ package org.apache.fineract.batch.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class ClientDetailsNotFoundException extends AbstractPlatformDomainRuleException {
public class BatchReferenceInvalidException extends AbstractPlatformDomainRuleException {
public ClientDetailsNotFoundException() {
super("validation.msg.batch.jlg.no.clients.defined", "No Client details found", "");
public BatchReferenceInvalidException() {
super("validation.msg.batch.root.invalid", "Root request not found");
}
public BatchReferenceInvalidException(Long reference) {
super("validation.msg.batch.reference.invalid", "Referenced request not found", reference);
}
}
@@ -21,17 +21,31 @@ package org.apache.fineract.batch.exception;
import static org.springframework.core.ResolvableType.forClassWithGenerics;
import com.google.gson.Gson;
import jakarta.persistence.PersistenceException;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import java.text.ParseException;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.naming.AuthenticationException;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.fortuna.ical4j.validate.ValidationException;
import org.apache.commons.collections4.SetUtils;
import org.apache.fineract.batch.domain.Header;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.exceptionmapper.DefaultExceptionMapper;
import org.apache.fineract.infrastructure.core.exceptionmapper.FineractExceptionMapper;
import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.core.NestedRuntimeException;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.stereotype.Component;
/**
@@ -55,7 +69,8 @@ public final class ErrorHandler {
private static final Gson JSON_HELPER = GoogleGsonSerializerHelper.createGsonBuilder(true).create();
private <T extends RuntimeException> ExceptionMapper<T> findMostSpecificExceptionHandler(T exception) {
@NotNull
public <T extends RuntimeException> ExceptionMapper<T> findMostSpecificExceptionHandler(T exception) {
Class<?> clazz = exception.getClass();
do {
Set<String> exceptionMappers = createSet(ctx.getBeanNamesForType(forClassWithGenerics(ExceptionMapper.class, clazz)));
@@ -71,6 +86,60 @@ public final class ErrorHandler {
return (ExceptionMapper<T>) defaultExceptionMapper;
}
/**
* Returns an object of ErrorInfo type containing the information regarding the raised error.
*
* @param exception
* @return ErrorInfo
*/
public ErrorInfo handle(@NotNull RuntimeException exception) {
ExceptionMapper<RuntimeException> exceptionMapper = findMostSpecificExceptionHandler(exception);
Response response = exceptionMapper.toResponse(exception);
MultivaluedMap<String, Object> headers = response.getHeaders();
Set<Header> batchHeaders = headers == null ? null
: headers.keySet().stream().map(e -> new Header(e, response.getHeaderString(e))).collect(Collectors.toSet());
return new ErrorInfo(response.getStatus(), ((FineractExceptionMapper) exceptionMapper).errorCode(),
JSON_HELPER.toJson(response.getEntity()), batchHeaders);
}
public RuntimeException getMappable(@NotNull Throwable thr) {
return getMappable(thr, null, null, null);
}
public RuntimeException getMappable(@NotNull Throwable t, String msgCode, String defaultMsg, String param,
final Object... defaultMsgArgs) {
String msg = defaultMsg == null ? t.getMessage() : defaultMsg;
param = param == null ? "unknown" : param;
if (t instanceof NestedRuntimeException nre) {
Throwable cause = nre.getMostSpecificCause();
msg = cause.getMessage();
if (nre instanceof NonTransientDataAccessException) {
msgCode = msgCode == null ? "error.msg." + param + ".data.integrity.issue" : msgCode;
return new PlatformDataIntegrityException(msgCode, msg, param, defaultMsgArgs);
}
}
if (t instanceof ValidationException) {
msgCode = msgCode == null ? "error.msg.validation." + param + ".error" : msgCode;
return new PlatformApiDataValidationException(List.of(ApiParameterError.parameterError(msgCode, msg, param, defaultMsgArgs)));
}
if (t instanceof PersistenceException) {
msgCode = msgCode == null ? "error.msg.persistence." + param + ".error" : msgCode;
return new PlatformDataIntegrityException(msgCode, msg, param, defaultMsgArgs);
}
if (t instanceof AuthenticationException) {
msgCode = msgCode == null ? "error.msg.authentication." + param + ".error" : msgCode;
return new PlatformDataIntegrityException(msgCode, msg, param, defaultMsgArgs);
}
if (t instanceof RuntimeException re) {
return re;
}
if (t instanceof ParseException) {
msgCode = msgCode == null ? "error.msg.parse." + param + ".error" : msgCode;
return new PlatformDataIntegrityException(msgCode, msg, param, defaultMsgArgs);
}
return new RuntimeException(msg, t);
}
private <T> Set<T> createSet(T[] array) {
if (array == null) {
return Set.of();
@@ -78,17 +147,4 @@ public final class ErrorHandler {
return Set.of(array);
}
}
/**
* Returns an object of ErrorInfo type containing the information regarding the raised error.
*
* @param exception
* @return ErrorInfo
*/
public ErrorInfo handle(final RuntimeException exception) {
ExceptionMapper<RuntimeException> exceptionMapper = findMostSpecificExceptionHandler(exception);
Response response = exceptionMapper.toResponse(exception);
return new ErrorInfo(response.getStatus(), ((FineractExceptionMapper) exceptionMapper).errorCode(),
JSON_HELPER.toJson(response.getEntity()));
}
}
@@ -18,6 +18,15 @@
*/
package org.apache.fineract.batch.exception;
import static lombok.AccessLevel.PROTECTED;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.batch.domain.Header;
/**
* Provides members to hold the basic information about the exceptions raised in commandStrategy classes.
*
@@ -25,84 +34,14 @@ package org.apache.fineract.batch.exception;
*
* @see ErrorHandler
*/
@Getter
@Setter(PROTECTED)
@NoArgsConstructor(access = PROTECTED)
@AllArgsConstructor
public final class ErrorInfo {
private Integer statusCode;
private Integer errorCode;
private String message;
/**
* Constructor to initialize the members of this class.
*
* @param statusCode
* @param errorCode
* @param message
*/
public ErrorInfo(final Integer statusCode, final Integer errorCode, final String message) {
this.statusCode = statusCode;
this.errorCode = errorCode;
this.message = message;
}
/**
* Constructor so JSON serialization will work with out special Serialiazer
*/
ErrorInfo() {
}
/**
* Getter method to provide the statusCode for an object of this type.
*
* @return Integer
*/
public Integer getStatusCode() {
return this.statusCode;
}
/**
* Setter method to set the statusCode for an object of this type.
*
* @param statusCode
*/
public void setStatusCode(final Integer statusCode) {
this.statusCode = statusCode;
}
/**
* Getter method to provide the errorCode for an object of this type.
*
* @return Integer
*/
public Integer getErrorCode() {
return this.errorCode;
}
/**
* Setter method to set the errorCode for an object of this type.
*
* @param errorCode
*/
public void setErrorCode(final Integer errorCode) {
this.errorCode = errorCode;
}
/**
* Getter method to provide the message of the error for an object of this type.
*
* @return String
*/
public String getMessage() {
return this.message;
}
/**
* Setter method to set the message of the error for an object of this type.
*
* @param message
*/
public void setMessage(final String message) {
this.message = message;
}
private Set<Header> headers;
}
@@ -18,23 +18,23 @@
*/
package org.apache.fineract.batch.service;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
import static org.apache.http.HttpStatus.SC_OK;
import com.google.gson.Gson;
import com.jayway.jsonpath.JsonPathException;
import io.github.resilience4j.core.functions.Either;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.core.UriInfo;
import java.util.ArrayList;
import java.util.Arrays;
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.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
@@ -47,25 +47,25 @@ 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.BatchReferenceInvalidException;
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.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.apache.fineract.infrastructure.core.filters.BatchRequestPreprocessor;
import org.jetbrains.annotations.NotNull;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionExecution;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.TransactionSystemException;
import org.springframework.transaction.support.TransactionTemplate;
/**
@@ -94,6 +94,64 @@ public class BatchApiServiceImpl implements BatchApiService {
@PersistenceContext
private final EntityManager entityManager;
/**
* Run each request root step in a separated transaction
*
* @param requestList
* @param uriInfo
* @return
*/
@Override
public List<BatchResponse> handleBatchRequestsWithoutEnclosingTransaction(final List<BatchRequest> requestList, UriInfo uriInfo) {
BatchRequestContextHolder.setEnclosingTransaction(Optional.empty());
return handleBatchRequests(false, requestList, uriInfo);
}
/**
* Run the batch request in transaction
*
* @param requestList
* @param uriInfo
* @return
*/
@Override
public List<BatchResponse> handleBatchRequestsWithEnclosingTransaction(final List<BatchRequest> requestList, final UriInfo uriInfo) {
return callInTransaction(Function.identity()::apply, () -> handleBatchRequests(true, requestList, uriInfo));
}
/**
* Helper method to run the command in transaction
*
* @param request
* the enclosing supplier of the command
* @param transactionConfigurator
* consumer to configure the transaction behavior and isolation
* @return
*/
private List<BatchResponse> callInTransaction(Consumer<TransactionTemplate> transactionConfigurator,
Supplier<List<BatchResponse>> request) {
List<BatchResponse> responseList = new ArrayList<>();
try {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionConfigurator.accept(transactionTemplate);
return transactionTemplate.execute(status -> {
BatchRequestContextHolder.setEnclosingTransaction(Optional.of(status));
try {
responseList.addAll(request.get());
return responseList;
} catch (BatchExecutionException ex) {
status.setRollbackOnly();
return List.of(buildErrorResponse(ex.getCause(), ex.getRequest()));
} catch (RuntimeException ex) {
status.setRollbackOnly();
return buildErrorResponses(ex, responseList);
}
});
} catch (TransactionException | NonTransientDataAccessException ex) {
return buildErrorResponses(ex, responseList);
}
}
/**
* 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.
@@ -104,29 +162,24 @@ public class BatchApiServiceImpl implements BatchApiService {
*/
private List<BatchResponse> handleBatchRequests(boolean enclosingTransaction, final List<BatchRequest> requestList,
final UriInfo uriInfo) {
final List<BatchResponse> responseList = new ArrayList<>(requestList.size());
final List<BatchRequestNode> batchRequestNodes = this.resolutionHelper.getDependingRequests(requestList);
if (batchRequestNodes.isEmpty()) {
final BatchResponse response = new BatchResponse();
ErrorInfo ex = errorHandler.handle(new ClientDetailsNotFoundException());
response.setStatusCode(500);
response.setBody(ex.getMessage());
responseList.add(response);
return responseList;
final List<BatchRequestNode> rootNodes;
try {
rootNodes = this.resolutionHelper.buildNodesTree(requestList);
} catch (BatchReferenceInvalidException e) {
return List.of(buildErrorResponse(e));
}
for (BatchRequestNode rootNode : batchRequestNodes) {
final ArrayList<BatchResponse> responseList = new ArrayList<>(requestList.size());
for (BatchRequestNode rootNode : rootNodes) {
if (enclosingTransaction) {
this.callRequestRecursive(rootNode.getRequest(), rootNode, responseList, uriInfo, enclosingTransaction);
} else {
List<BatchResponse> localResponseList = new ArrayList<>();
ArrayList<BatchResponse> localResponseList = new ArrayList<>();
this.callRequestRecursive(rootNode.getRequest(), rootNode, localResponseList, uriInfo, enclosingTransaction);
responseList.addAll(localResponseList);
}
}
Collections.sort(responseList, Comparator.comparing(BatchResponse::getRequestId));
responseList.sort(Comparator.comparing(BatchResponse::getRequestId));
return responseList;
}
@@ -143,7 +196,7 @@ public class BatchApiServiceImpl implements BatchApiService {
*/
private void callRequestRecursive(BatchRequest request, BatchRequestNode requestNode, List<BatchResponse> responseList, UriInfo uriInfo,
boolean enclosingTransaction) {
// 1. run current node
// run current node
BatchResponse response;
if (enclosingTransaction) {
response = executeRequest(request, uriInfo);
@@ -154,54 +207,28 @@ public class BatchApiServiceImpl implements BatchApiService {
response = transactionResponse.get(0);
}
responseList.add(response);
if (response.getStatusCode() != null && response.getStatusCode() == 200) {
requestNode.getChildRequests().forEach(childNode -> {
if (response.getStatusCode() != null && response.getStatusCode() == SC_OK) {
// run child nodes
requestNode.getChildNodes().forEach(childNode -> {
BatchRequest childRequest = childNode.getRequest();
BatchRequest resolvedChildRequest;
try {
resolvedChildRequest = this.resolutionHelper.resoluteRequest(childNode.getRequest(), response);
} catch (JsonPathException jsonPathException) {
BatchResponse childResponse = new BatchResponse().setRequestId(childNode.getRequest().getRequestId())
.setHeaders(childNode.getRequest().getHeaders()).setStatusCode(BAD_REQUEST.value())
.setBody(jsonPathException.getMessage());
responseList.add(childResponse);
resolvedChildRequest = this.resolutionHelper.resolveRequest(childRequest, response);
} catch (JsonPathException jpex) {
responseList.add(buildErrorResponse(jpex, childRequest));
return;
} catch (RuntimeException ex) {
throw new BatchExecutionException(childNode.getRequest(), ex, errorHandler.handle(ex));
throw new BatchExecutionException(childRequest, ex);
}
callRequestRecursive(resolvedChildRequest, childNode, responseList, uriInfo, enclosingTransaction);
});
} else {
responseList.addAll(parentRequestFailedRecursive(request, requestNode));
responseList.addAll(parentRequestFailedRecursive(request, requestNode, response, true));
}
// If the current request fails, then all the child requests are not executed. If we want to write out all the
// child requests, here is the place.
}
/**
* All requests recursively are set to status 409 if the parent request fails.
*
* @param request
* the current request
* @param requestNode
* the current request node
* @return {@code BatchResponse} list of the generated batch responses
*/
private List<BatchResponse> parentRequestFailedRecursive(BatchRequest request, BatchRequestNode requestNode) {
List<BatchResponse> responseList = new ArrayList<>();
BatchRequestContextHolder.getEnclosingTransaction().ifPresent(TransactionExecution::setRollbackOnly);
BatchResponse errorResponse = new BatchResponse();
errorResponse.setRequestId(request.getRequestId());
errorResponse.setStatusCode(Status.CONFLICT.getStatusCode());
// Some detail information about the error
final ErrorInfo conflictError = new ErrorInfo(Status.CONFLICT.getStatusCode(), 8001,
"Parent request with id " + request.getRequestId() + " was erroneous!");
errorResponse.setBody(conflictError.getMessage());
requestNode.getChildRequests().stream()
.flatMap(childNode -> parentRequestFailedRecursive(childNode.getRequest(), childNode).stream()).forEach(responseList::add);
return responseList;
}
/**
* Execute the request
*
@@ -215,7 +242,7 @@ public class BatchApiServiceImpl implements BatchApiService {
log.debug("Batch request: method [{}], relative url [{}]", request.getMethod(), request.getRelativeUrl());
Either<RuntimeException, BatchRequest> preprocessorResult = runPreprocessors(request);
if (preprocessorResult.isLeft()) {
throw new BatchExecutionException(request, preprocessorResult.getLeft(), errorHandler.handle(preprocessorResult.getLeft()));
throw new BatchExecutionException(request, preprocessorResult.getLeft());
} else {
request = preprocessorResult.get();
}
@@ -224,13 +251,8 @@ public class BatchApiServiceImpl implements BatchApiService {
.map(list -> list.stream().collect(Collectors.toMap(Header::getName, Header::getValue)))
.orElse(Collections.emptyMap())));
BatchCallHandler callHandler = new BatchCallHandler(this.batchFilters, commandStrategy::execute);
if (BatchRequestContextHolder.getEnclosingTransaction().isPresent()) {
if (BatchRequestContextHolder.getEnclosingTransaction().get().isRollbackOnly()) {
BatchResponse br = new BatchResponse();
br.setBody("Parent request was erroneous!");
br.setRequestId(request.getRequestId());
return br;
}
Optional<TransactionStatus> transaction = BatchRequestContextHolder.getEnclosingTransaction();
if (transaction.isPresent()) {
entityManager.flush();
}
final BatchResponse rootResponse = callHandler.serviceCall(request, uriInfo);
@@ -238,9 +260,9 @@ public class BatchApiServiceImpl implements BatchApiService {
request.getRelativeUrl());
return rootResponse;
} catch (AbstractIdempotentCommandException idempotentException) {
return handleIdempotentRequests(idempotentException, request);
return buildErrorResponse(idempotentException, request);
} catch (RuntimeException ex) {
throw new BatchExecutionException(request, ex, errorHandler.handle(ex));
throw new BatchExecutionException(request, ex);
} finally {
BatchRequestContextHolder.resetRequestAttributes();
}
@@ -266,126 +288,97 @@ public class BatchApiServiceImpl implements BatchApiService {
}
/**
* Return the idempotent response when idempotent exception raised
* All requests recursively are set to status 409 if the parent request fails.
*
* @param idempotentException
* the idempotent exception
* @param request
* the called request
* @return
* the current request
* @param requestNode
* the current request node
* @return {@code BatchResponse} list of the generated batch responses
*/
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());
private List<BatchResponse> parentRequestFailedRecursive(BatchRequest request, BatchRequestNode requestNode, BatchResponse response,
boolean root) {
List<BatchResponse> responseList = new ArrayList<>();
if (root) {
BatchRequestContextHolder.getEnclosingTransaction().ifPresent(TransactionExecution::setRollbackOnly);
} else {
response.setStatusCode(500);
responseList.add(buildErrorResponse(request.getRequestId(), response.getStatusCode(), response.getBody(), null));
}
return response;
}
/**
* Run each request root step in a separated transaction
*
* @param requestList
* @param uriInfo
* @return
*/
@Override
public List<BatchResponse> handleBatchRequestsWithoutEnclosingTransaction(final List<BatchRequest> requestList, UriInfo uriInfo) {
BatchRequestContextHolder.setEnclosingTransaction(Optional.empty());
return handleBatchRequests(false, requestList, uriInfo);
}
/**
* Run the batch request in transaction
*
* @param requestList
* @param uriInfo
* @return
*/
@Override
public List<BatchResponse> handleBatchRequestsWithEnclosingTransaction(final List<BatchRequest> requestList, final UriInfo uriInfo) {
return callInTransaction(() -> handleBatchRequests(true, requestList, uriInfo));
requestNode.getChildNodes().forEach(
childNode -> responseList.addAll(parentRequestFailedRecursive(childNode.getRequest(), childNode, response, false)));
return responseList;
}
@NotNull
private List<BatchResponse> createErrorResponse(List<BatchResponse> responseList, int statusCode) {
BatchResponse errResponse = new BatchResponse();
for (BatchResponse res : responseList) {
if (res.getStatusCode() == null || !res.getStatusCode().equals(200)) {
errResponse.setBody("Transaction is being rolled back. First erroneous request: \n" + new Gson().toJson(res));
errResponse.setRequestId(res.getRequestId());
if (statusCode == -1) {
if (res.getStatusCode() != null) {
statusCode = res.getStatusCode();
} else {
statusCode = Status.INTERNAL_SERVER_ERROR.getStatusCode();
}
}
break;
}
}
errResponse.setStatusCode(statusCode);
return Arrays.asList(errResponse);
}
private List<BatchResponse> callInTransaction(Supplier<List<BatchResponse>> request) {
return callInTransaction(Function.identity()::apply, request);
private BatchResponse buildErrorResponse(@NotNull Throwable ex) {
return buildErrorResponse(ex, null);
}
/**
* Helper method to run the command in transaction
* Return the response when any exception raised
*
* @param ex
* the exception
* @param request
* the enclosing supplier of the command
* @param transactionConfigurator
* consumer to configure the transaction behavior and isolation
* @return
* the called request
*/
private List<BatchResponse> callInTransaction(Consumer<TransactionTemplate> transactionConfigurator,
Supplier<List<BatchResponse>> request) {
List<BatchResponse> responseList = new ArrayList<>();
try {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionConfigurator.accept(transactionTemplate);
return transactionTemplate.execute(status -> {
BatchRequestContextHolder.setEnclosingTransaction(Optional.of(status));
try {
responseList.addAll(request.get());
if (status.isRollbackOnly()) {
return createErrorResponse(responseList, -1);
}
return responseList;
} catch (BatchExecutionException ex) {
status.setRollbackOnly();
BatchResponse errResponse = new BatchResponse();
errResponse.setStatusCode(ex.getErrorInfo().getStatusCode());
errResponse.setRequestId(ex.getRequest().getRequestId());
errResponse.setBody(ex.getErrorInfo().getMessage());
return Arrays.asList(errResponse);
} catch (RuntimeException ex) {
status.setRollbackOnly();
ErrorInfo e = errorHandler.handle(ex);
BatchResponse errResponse = new BatchResponse();
errResponse.setStatusCode(e.getStatusCode());
errResponse.setBody(e.getMessage());
return Arrays.asList(errResponse);
}
});
} catch (TransactionException | NonTransientDataAccessException ex) {
ErrorInfo e = errorHandler.handle(ex);
return createErrorResponse(responseList, e.getStatusCode());
private BatchResponse buildErrorResponse(Throwable ex, BatchRequest request) {
Long requestId = null;
Integer statusCode = null;
String body = null;
Set<Header> headers = new HashSet<>();
if (ex != null) {
ErrorInfo errorInfo = errorHandler.handle(errorHandler.getMappable(ex));
statusCode = errorInfo.getStatusCode();
body = errorInfo.getMessage();
headers = Optional.ofNullable(errorInfo.getHeaders()).orElse(new HashSet<>());
}
if (request != null) {
requestId = request.getRequestId();
if (request.getHeaders() != null) {
headers.addAll(request.getHeaders());
}
}
return buildErrorResponse(requestId, statusCode, body, headers);
}
@NotNull
private List<BatchResponse> buildErrorResponses(Throwable ex, @NotNull List<BatchResponse> responseList) {
BatchResponse response = responseList.isEmpty() ? null
: responseList.stream().filter(e -> e.getStatusCode() == null || e.getStatusCode() != SC_OK).findFirst()
.orElse(responseList.get(responseList.size() - 1));
if (response != null && response.getStatusCode() == SC_OK && ex instanceof TransactionSystemException tse) {
ex = new ConcurrencyFailureException(tse.getMessage(), tse.getCause());
}
Long requestId = null;
Integer statusCode = null;
String body = null;
Set<Header> headers = new HashSet<>();
if (ex != null) {
ErrorInfo errorInfo = errorHandler.handle(errorHandler.getMappable(ex));
statusCode = errorInfo.getStatusCode();
body = errorInfo.getMessage();
headers = Optional.ofNullable(errorInfo.getHeaders()).orElse(new HashSet<>());
}
if (response != null) {
requestId = response.getRequestId();
if (response.getStatusCode() == null || response.getStatusCode() != SC_OK) {
if (response.getStatusCode() != null) {
statusCode = response.getStatusCode();
}
body = "Transaction is being rolled back. First erroneous request: \n" + new Gson().toJson(response);
}
if (response.getHeaders() != null) {
headers.addAll(response.getHeaders());
}
}
return List.of(buildErrorResponse(requestId, statusCode, body, headers));
}
private BatchResponse buildErrorResponse(Long requestId, Integer statusCode, String body, Set<Header> headers) {
return new BatchResponse().setRequestId(requestId).setStatusCode(statusCode == null ? SC_INTERNAL_SERVER_ERROR : statusCode)
.setBody(body == null ? "Request with id " + requestId + " was erroneous!" : body).setHeaders(headers);
}
}
@@ -20,17 +20,14 @@ package org.apache.fineract.batch.service;
import lombok.Getter;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.exception.ErrorInfo;
@Getter
public class BatchExecutionException extends RuntimeException {
private final BatchRequest request;
private final ErrorInfo errorInfo;
public BatchExecutionException(BatchRequest request, RuntimeException ex, ErrorInfo errorInfo) {
public BatchExecutionException(BatchRequest request, RuntimeException ex) {
super("Error executing batch request: " + request, ex);
this.request = request;
this.errorInfo = errorInfo;
}
}
@@ -30,6 +30,7 @@ import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse;
import org.apache.fineract.batch.exception.BatchReferenceInvalidException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.springframework.stereotype.Component;
@@ -55,23 +56,19 @@ public class ResolutionHelper {
private BatchRequest request;
private final List<BatchRequestNode> childRequests = new ArrayList<>();
public BatchRequestNode() {
public BatchRequestNode(BatchRequest request) {
this.request = request;
}
public BatchRequest getRequest() {
return this.request;
}
public void setRequest(BatchRequest request) {
this.request = request;
}
public List<BatchRequestNode> getChildRequests() {
public List<BatchRequestNode> getChildNodes() {
return this.childRequests;
}
public void addChildRequest(final BatchRequestNode batchRequest) {
public void addChildNode(final BatchRequestNode batchRequest) {
this.childRequests.add(batchRequest);
}
@@ -84,35 +81,37 @@ public class ResolutionHelper {
* different list is identified with a "Key" which is the "requestId" of the request at topmost level in dependency
* hierarchy of that particular list.
*
* @param batchRequests
* @param requests
* @return List&lt;ArrayList&lt;BatchRequestNode&gt;&gt;
*/
public List<BatchRequestNode> getDependingRequests(final List<BatchRequest> batchRequests) {
final List<BatchRequestNode> rootRequests = new ArrayList<>();
for (BatchRequest batchRequest : batchRequests) {
if (batchRequest.getReference() == null) {
final BatchRequestNode node = new BatchRequestNode();
node.setRequest(batchRequest);
rootRequests.add(node);
public List<BatchRequestNode> buildNodesTree(final List<BatchRequest> requests) {
final List<BatchRequestNode> rootNodes = new ArrayList<>();
for (BatchRequest request : requests) {
if (request.getReference() == null) {
final BatchRequestNode node = new BatchRequestNode(request);
rootNodes.add(node);
} else {
this.addDependingRequest(batchRequest, rootRequests);
if (!addDependingRequest(request, rootNodes)) {
throw new BatchReferenceInvalidException(request.getReference());
}
}
}
return rootRequests;
return rootNodes;
}
private void addDependingRequest(final BatchRequest batchRequest, final List<BatchRequestNode> parentRequests) {
for (BatchRequestNode batchRequestNode : parentRequests) {
if (batchRequestNode.getRequest().getRequestId().equals(batchRequest.getReference())) {
final BatchRequestNode dependingRequest = new BatchRequestNode();
dependingRequest.setRequest(batchRequest);
batchRequestNode.addChildRequest(dependingRequest);
private boolean addDependingRequest(final BatchRequest request, final List<BatchRequestNode> parentNodes) {
for (BatchRequestNode parentNode : parentNodes) {
if (parentNode.getRequest().getRequestId().equals(request.getReference())) {
final BatchRequestNode childNode = new BatchRequestNode(request);
parentNode.addChildNode(childNode);
return true;
} else {
addDependingRequest(batchRequest, batchRequestNode.getChildRequests());
if (addDependingRequest(request, parentNode.getChildNodes())) {
return true;
}
}
}
return false;
}
/**
@@ -123,34 +122,27 @@ public class ResolutionHelper {
* @param parentResponse
* @return BatchRequest
*/
public BatchRequest resoluteRequest(final BatchRequest request, final BatchResponse parentResponse) {
// Create a duplicate request
final BatchRequest br = request;
public BatchRequest resolveRequest(final BatchRequest request, final BatchResponse parentResponse) {
final ReadContext responseCtx = JsonPath.parse(parentResponse.getBody());
// Gets the body from current Request as a JsonObject
final JsonObject jsonRequestBody = this.fromJsonHelper.parse(request.getBody()).getAsJsonObject();
JsonObject jsonResultBody = new JsonObject();
// Iterate through each element in the requestBody to find dependent
// parameter
for (Map.Entry<String, JsonElement> element : jsonRequestBody.entrySet()) {
final String key = element.getKey();
final JsonElement value = resolveDependentVariables(element, responseCtx);
jsonResultBody.add(key, value);
String requestBody = request.getBody();
if (requestBody != null) {
final JsonObject jsonRequestBody = this.fromJsonHelper.parse(requestBody).getAsJsonObject();
JsonObject jsonResultBody = new JsonObject();
// Iterate through each element in the requestBody to find dependent
// parameter
for (Map.Entry<String, JsonElement> element : jsonRequestBody.entrySet()) {
final String key = element.getKey();
final JsonElement value = resolveDependentVariables(element, responseCtx);
jsonResultBody.add(key, value);
}
// Set the body after dependency resolution
request.setBody(jsonResultBody.toString());
}
// Set the body after dependency resolution
br.setBody(jsonResultBody.toString());
// Also check the relativeUrl for any dependency resolution
String relativeUrl = request.getRelativeUrl();
if (relativeUrl.contains("$.")) {
String queryParams = "";
if (relativeUrl.contains("?")) {
queryParams = relativeUrl.substring(relativeUrl.indexOf("?"));
@@ -158,24 +150,21 @@ public class ResolutionHelper {
}
final Iterable<String> parameters = Splitter.on('/').split(relativeUrl);
for (String parameter : parameters) {
if (parameter.contains("$.")) {
final String resParamValue = responseCtx.read(parameter).toString();
relativeUrl = relativeUrl.replace(parameter, resParamValue);
br.setRelativeUrl(relativeUrl + queryParams);
request.setRelativeUrl(relativeUrl + queryParams);
}
}
}
return br;
return request;
}
private JsonElement resolveDependentVariables(final Map.Entry<String, JsonElement> entryElement, final ReadContext responseCtx) {
JsonElement value = null;
JsonElement value;
final JsonElement element = entryElement.getValue();
if (element.isJsonObject()) {
final JsonObject jsObject = element.getAsJsonObject();
value = processJsonObject(jsObject, responseCtx);
@@ -202,16 +191,13 @@ public class ResolutionHelper {
}
private JsonArray processJsonArray(final JsonArray elementArray, final ReadContext responseCtx) {
JsonArray valueArr = new JsonArray();
for (JsonElement element : elementArray) {
if (element.isJsonObject()) {
final JsonObject jsObject = element.getAsJsonObject();
valueArr.add(processJsonObject(jsObject, responseCtx));
}
}
return valueArr;
}
@@ -123,16 +123,6 @@ public class CommandSource extends AbstractPersistableCustom {
@Column(name = "result_status_code")
private Integer resultStatusCode;
public static CommandSource fullEntryFrom(final CommandWrapper wrapper, final JsonCommand command, final AppUser maker,
String idempotencyKey, Integer status) {
return new CommandSource(wrapper.actionName(), wrapper.entityName(), wrapper.getHref(), command.entityId(), command.subentityId(),
command.json(), maker, idempotencyKey, status);
}
protected CommandSource() {
//
}
private CommandSource(final String actionName, final String entityName, final String href, final Long resourceId,
final Long subResourceId, final String commandSerializedAsJson, final AppUser maker, final String idempotencyKey,
final Integer status) {
@@ -148,6 +138,26 @@ public class CommandSource extends AbstractPersistableCustom {
this.idempotencyKey = idempotencyKey;
}
public static CommandSource fullEntryFrom(final CommandWrapper wrapper, final JsonCommand command, final AppUser maker,
String idempotencyKey, Integer status) {
CommandSource commandSource = new CommandSource(wrapper.actionName(), wrapper.entityName(), wrapper.getHref(), command.entityId(),
command.subentityId(), command.json(), maker, idempotencyKey, status);
commandSource.officeId = wrapper.getOfficeId();
commandSource.groupId = command.getGroupId();
commandSource.clientId = command.getClientId();
commandSource.loanId = command.getLoanId();
commandSource.savingsId = command.getSavingsId();
commandSource.productId = command.getProductId();
commandSource.transactionId = command.getTransactionId();
commandSource.creditBureauId = command.getCreditBureauId();
commandSource.organisationCreditBureauId = command.getOrganisationCreditBureauId();
return commandSource;
}
protected CommandSource() {
//
}
public Long getCreditBureauId() {
return this.creditBureauId;
}
@@ -160,31 +170,27 @@ public class CommandSource extends AbstractPersistableCustom {
return this.organisationCreditBureauId;
}
public String getJobName() {
return this.jobName;
}
public void setOrganisationCreditBureauId(Long organisationCreditBureauId) {
this.organisationCreditBureauId = organisationCreditBureauId;
}
public void markAsChecked(final AppUser checker) {
this.checker = checker;
this.checkedOnDate = DateUtils.getOffsetDateTimeOfTenantWithMostPrecision();
this.status = CommandProcessingResultType.PROCESSED.getValue();
public String getJobName() {
return this.jobName;
}
public void markAsRejected(final AppUser checker) {
this.checker = checker;
this.checkedOnDate = DateUtils.getOffsetDateTimeOfTenantWithMostPrecision();
this.status = CommandProcessingResultType.REJECTED.getValue();
public Long getResourceId() {
return this.resourceId;
}
public void updateResourceId(final Long resourceId) {
public void setResourceId(final Long resourceId) {
this.resourceId = resourceId;
}
public void updateSubResourceId(final Long subResourceId) {
public Long getSubResourceId() {
return this.subResourceId;
}
public void setSubResourceId(final Long subResourceId) {
this.subResourceId = subResourceId;
}
@@ -196,14 +202,6 @@ public class CommandSource extends AbstractPersistableCustom {
this.commandAsJson = json;
}
public Long resourceId() {
return this.resourceId;
}
public Long subResourceId() {
return this.subResourceId;
}
public String getActionName() {
return this.actionName;
}
@@ -216,36 +214,6 @@ public class CommandSource extends AbstractPersistableCustom {
return this.actionName + "_" + this.entityName;
}
public Long getResourceId() {
return this.resourceId;
}
public Long getSubResourceId() {
return this.subResourceId;
}
public void markAsAwaitingApproval() {
this.status = CommandProcessingResultType.AWAITING_APPROVAL.getValue();
}
public boolean isMarkedAsAwaitingApproval() {
return this.status.equals(CommandProcessingResultType.AWAITING_APPROVAL.getValue());
}
public void updateForAudit(final CommandProcessingResult result) {
this.officeId = result.getOfficeId();
this.groupId = result.getGroupId();
this.clientId = result.getClientId();
this.loanId = result.getLoanId();
this.savingsId = result.getSavingsId();
this.productId = result.getProductId();
this.transactionId = result.getTransactionId();
this.resourceId = result.getResourceId();
this.resourceExternalId = result.getResourceExternalId();
this.subResourceId = result.getSubResourceId();
this.subResourceExternalId = result.getSubResourceExternalId();
}
public String getResourceGetUrl() {
return this.resourceGetUrl;
}
@@ -296,7 +264,7 @@ public class CommandSource extends AbstractPersistableCustom {
return this.transactionId;
}
public void updateTransaction(final String transactionId) {
public void setTransactionId(final String transactionId) {
this.transactionId = transactionId;
}
@@ -324,6 +292,10 @@ public class CommandSource extends AbstractPersistableCustom {
this.status = status;
}
public void setStatus(CommandProcessingResultType status) {
setStatus(status == null ? null : status.getValue());
}
public Integer getResultStatusCode() {
return resultStatusCode;
}
@@ -331,4 +303,38 @@ public class CommandSource extends AbstractPersistableCustom {
public void setResultStatusCode(Integer resultStatusCode) {
this.resultStatusCode = resultStatusCode;
}
public void markAsAwaitingApproval() {
this.status = CommandProcessingResultType.AWAITING_APPROVAL.getValue();
}
public boolean isMarkedAsAwaitingApproval() {
return this.status.equals(CommandProcessingResultType.AWAITING_APPROVAL.getValue());
}
public void markAsChecked(final AppUser checker) {
this.checker = checker;
this.checkedOnDate = DateUtils.getOffsetDateTimeOfTenantWithMostPrecision();
this.status = CommandProcessingResultType.PROCESSED.getValue();
}
public void markAsRejected(final AppUser checker) {
this.checker = checker;
this.checkedOnDate = DateUtils.getOffsetDateTimeOfTenantWithMostPrecision();
this.status = CommandProcessingResultType.REJECTED.getValue();
}
public void updateForAudit(final CommandProcessingResult result) {
this.officeId = result.getOfficeId();
this.groupId = result.getGroupId();
this.clientId = result.getClientId();
this.loanId = result.getLoanId();
this.savingsId = result.getSavingsId();
this.productId = result.getProductId();
this.transactionId = result.getTransactionId();
this.resourceId = result.getResourceId();
this.resourceExternalId = result.getResourceExternalId();
this.subResourceId = result.getSubResourceId();
this.subResourceExternalId = result.getSubResourceExternalId();
}
}
@@ -302,6 +302,10 @@ public class CommandWrapper {
return this.taskPermissionName;
}
public Long getOfficeId() {
return officeId;
}
public Long getGroupId() {
return this.groupId;
}
@@ -18,7 +18,6 @@
*/
package org.apache.fineract.commands.service;
import static org.apache.fineract.commands.domain.CommandProcessingResultType.ERROR;
import static org.apache.fineract.commands.domain.CommandProcessingResultType.UNDER_PROCESSING;
import lombok.RequiredArgsConstructor;
@@ -48,46 +47,48 @@ public class CommandSourceService {
private final CommandSourceRepository commandSourceRepository;
private final ErrorHandler errorHandler;
@NotNull
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ)
public CommandSource saveInitial(CommandWrapper wrapper, JsonCommand jsonCommand, AppUser maker, String idempotencyKey) {
return saveInitialInternal(wrapper, jsonCommand, maker, idempotencyKey);
}
public CommandSource saveInitialNoTransaction(CommandWrapper wrapper, JsonCommand jsonCommand, AppUser maker, String idempotencyKey) {
return saveInitialInternal(wrapper, jsonCommand, maker, idempotencyKey);
public CommandSource saveInitialNewTransaction(CommandWrapper wrapper, JsonCommand jsonCommand, AppUser maker, String idempotencyKey) {
return saveInitial(wrapper, jsonCommand, maker, idempotencyKey);
}
@NotNull
private CommandSource saveInitialInternal(CommandWrapper wrapper, JsonCommand jsonCommand, AppUser maker, String idempotencyKey) {
CommandSource initialCommandSource = getInitialCommandSource(wrapper, jsonCommand, maker, idempotencyKey);
@Transactional(propagation = Propagation.REQUIRED)
public CommandSource saveInitialSameTransaction(CommandWrapper wrapper, JsonCommand jsonCommand, AppUser maker, String idempotencyKey) {
return saveInitial(wrapper, jsonCommand, maker, idempotencyKey);
}
@NotNull
private CommandSource saveInitial(CommandWrapper wrapper, JsonCommand jsonCommand, AppUser maker, String idempotencyKey) {
CommandSource initialCommandSource = getInitialCommandSource(wrapper, jsonCommand, maker, idempotencyKey);
if (initialCommandSource.getCommandJson() == null) {
initialCommandSource.setCommandJson("{}");
}
return commandSourceRepository.saveAndFlush(initialCommandSource);
}
public void saveFailed(CommandSource commandSource) {
commandSource.setStatus(ERROR.getValue());
commandSourceRepository.saveAndFlush(commandSource);
}
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ)
public CommandSource saveResult(CommandSource commandSource) {
public CommandSource saveResultNewTransaction(@NotNull CommandSource commandSource) {
return saveResult(commandSource);
}
@Transactional(propagation = Propagation.REQUIRED)
public CommandSource saveResultSameTransaction(@NotNull CommandSource commandSource) {
return saveResult(commandSource);
}
@NotNull
private CommandSource saveResult(@NotNull CommandSource commandSource) {
return commandSourceRepository.saveAndFlush(commandSource);
}
public CommandSource saveResultNoTransaction(CommandSource commandSource) {
return commandSourceRepository.saveAndFlush(commandSource);
public ErrorInfo generateErrorInfo(Throwable t) {
return errorHandler.handle(errorHandler.getMappable(t));
}
public ErrorInfo generateErrorException(Throwable t) {
if (t instanceof final RuntimeException e) {
return errorHandler.handle(e);
} else {
return new ErrorInfo(500, 9999, "{\"Exception\": " + t.toString() + "}");
}
public CommandSource getCommandSource(Long commandSourceId) {
return commandSourceRepository.findById(commandSourceId).orElseThrow(() -> new CommandNotFoundException(commandSourceId));
}
public CommandSource findCommandSource(CommandWrapper wrapper, String idempotencyKey) {
@@ -82,15 +82,15 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
validateIsUpdateAllowed();
final CommandWrapper wrapper = CommandWrapper.fromExistingCommand(makerCheckerId, commandSourceInput.getActionName(),
commandSourceInput.getEntityName(), commandSourceInput.resourceId(), commandSourceInput.subResourceId(),
commandSourceInput.getEntityName(), commandSourceInput.getResourceId(), commandSourceInput.getSubResourceId(),
commandSourceInput.getResourceGetUrl(), commandSourceInput.getProductId(), commandSourceInput.getOfficeId(),
commandSourceInput.getGroupId(), commandSourceInput.getClientId(), commandSourceInput.getLoanId(),
commandSourceInput.getSavingsId(), commandSourceInput.getTransactionId(), commandSourceInput.getCreditBureauId(),
commandSourceInput.getOrganisationCreditBureauId(), commandSourceInput.getIdempotencyKey());
final JsonElement parsedCommand = this.fromApiJsonHelper.parse(commandSourceInput.getCommandJson());
final JsonCommand command = JsonCommand.fromExistingCommand(makerCheckerId, commandSourceInput.getCommandJson(), parsedCommand,
this.fromApiJsonHelper, commandSourceInput.getEntityName(), commandSourceInput.resourceId(),
commandSourceInput.subResourceId(), commandSourceInput.getGroupId(), commandSourceInput.getClientId(),
this.fromApiJsonHelper, commandSourceInput.getEntityName(), commandSourceInput.getResourceId(),
commandSourceInput.getSubResourceId(), commandSourceInput.getGroupId(), commandSourceInput.getClientId(),
commandSourceInput.getLoanId(), commandSourceInput.getSavingsId(), commandSourceInput.getTransactionId(),
commandSourceInput.getResourceGetUrl(), commandSourceInput.getProductId(), commandSourceInput.getCreditBureauId(),
commandSourceInput.getOrganisationCreditBureauId(), commandSourceInput.getJobName());
@@ -21,6 +21,7 @@ package org.apache.fineract.commands.service;
import static org.apache.fineract.commands.domain.CommandProcessingResultType.ERROR;
import static org.apache.fineract.commands.domain.CommandProcessingResultType.PROCESSED;
import static org.apache.fineract.commands.domain.CommandProcessingResultType.UNDER_PROCESSING;
import static org.apache.http.HttpStatus.SC_OK;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
@@ -32,6 +33,7 @@ import java.util.Map;
import java.util.function.Function;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.batch.exception.ErrorHandler;
import org.apache.fineract.batch.exception.ErrorInfo;
import org.apache.fineract.commands.domain.CommandProcessingResultType;
import org.apache.fineract.commands.domain.CommandSource;
@@ -79,6 +81,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
private final IdempotencyKeyResolver idempotencyKeyResolver;
private final IdempotencyKeyGenerator idempotencyKeyGenerator;
private final CommandSourceService commandSourceService;
private final ErrorHandler errorHandler;
private final FineractRequestContextHolder fineractRequestContextHolder;
private final Gson gson = GoogleGsonSerializerHelper.createSimpleGson();
@@ -90,90 +93,95 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
// Do not store the idempotency key because of the exception handling
setIdempotencyKeyStoreFlag(false);
final boolean rollbackTransaction = configurationDomainService.isMakerCheckerEnabledForTask(wrapper.taskPermissionName());
String idempotencyKey = idempotencyKeyResolver.resolve(wrapper);
exceptionWhenTheRequestAlreadyProcessed(wrapper, idempotencyKey);
Long commandId = (Long) fineractRequestContextHolder.getAttribute(COMMAND_SOURCE_ID, null);
boolean isRetry = commandId != null;
// Store idempotency key to the request attribute
CommandSource savedCommandSource;
if (BatchRequestContextHolder.getEnclosingTransaction().isPresent()) {
savedCommandSource = commandSourceService.saveInitialNoTransaction(wrapper, command, context.authenticatedUser(wrapper),
idempotencyKey);
CommandSource commandSource = null;
String idempotencyKey;
if (isRetry) {
commandSource = commandSourceService.getCommandSource(commandId);
idempotencyKey = commandSource.getIdempotencyKey();
} else {
savedCommandSource = commandSourceService.saveInitial(wrapper, command, context.authenticatedUser(wrapper), idempotencyKey);
idempotencyKey = idempotencyKeyResolver.resolve(wrapper);
}
exceptionWhenTheRequestAlreadyProcessed(wrapper, idempotencyKey, isRetry);
boolean sameTransaction = BatchRequestContextHolder.getEnclosingTransaction().isPresent();
if (commandSource == null) {
AppUser user = context.authenticatedUser(wrapper);
commandSource = sameTransaction ? commandSourceService.saveInitialSameTransaction(wrapper, command, user, idempotencyKey)
: commandSourceService.saveInitialNewTransaction(wrapper, command, user, idempotencyKey);
storeCommandIdInContext(commandSource); // Store command id as a request attribute
}
storeCommandToIdempotentFilter(savedCommandSource);
setIdempotencyKeyStoreFlag(true);
final CommandProcessingResult result;
try {
result = findCommandHandler(wrapper).processCommand(command);
} catch (Throwable t) { // NOSONAR
CommandSource source = commandSourceService.findCommandSource(wrapper, idempotencyKey);
commandSourceService.saveFailed(source);
publishHookErrorEvent(wrapper, command, t);
ErrorInfo errorInfo = commandSourceService.generateErrorInfo(t);
commandSource.setResultStatusCode(errorInfo.getStatusCode());
commandSource.setResult(errorInfo.getMessage());
commandSource.setStatus(ERROR);
commandSource = sameTransaction ? commandSourceService.saveResultSameTransaction(commandSource)
: commandSourceService.saveResultNewTransaction(commandSource);
publishHookErrorEvent(wrapper, command, errorInfo);
throw t;
}
CommandSource initialCommandSource = commandSourceService.findCommandSource(wrapper, idempotencyKey);
initialCommandSource.setResult(toApiJsonSerializer.serializeResult(result));
initialCommandSource.updateResourceId(result.getResourceId());
initialCommandSource.updateForAudit(result);
commandSource.updateForAudit(result);
commandSource.setResult(toApiJsonSerializer.serializeResult(result));
commandSource.setResultStatusCode(SC_OK);
commandSource.setStatus(PROCESSED);
boolean rollBack = (rollbackTransaction || result.isRollbackTransaction()) && !isApprovedByChecker;
if (result.hasChanges() && !rollBack) {
initialCommandSource.setCommandJson(toApiJsonSerializer.serializeResult(result.getChanges()));
boolean isRollback = !isApprovedByChecker && (result.isRollbackTransaction()
|| configurationDomainService.isMakerCheckerEnabledForTask(wrapper.taskPermissionName()));
// TODO: this should be removed, can not override audit information (and maker-checker does not work)
if (!isRollback && result.hasChanges()) {
commandSource.setCommandJson(toApiJsonSerializer.serializeResult(result.getChanges()));
}
initialCommandSource.setStatus(CommandProcessingResultType.PROCESSED.getValue());
if (BatchRequestContextHolder.getEnclosingTransaction().isPresent()) {
commandSourceService.saveResultNoTransaction(initialCommandSource);
} else {
commandSourceService.saveResult(initialCommandSource);
}
commandSource = commandSourceService.saveResultSameTransaction(commandSource);
if ((rollbackTransaction || result.isRollbackTransaction()) && !isApprovedByChecker) {
if (isRollback) {
/*
* JournalEntry will generate a new transactionId every time. Updating the transactionId with old
* transactionId, because as there are no entries are created with new transactionId, will throw an error
* when checker approves the transaction
*/
initialCommandSource.updateTransaction(command.getTransactionId());
/*
* Update CommandSource json data with JsonCommand json data, line 77 and 81 may update the json data
*/
initialCommandSource.setCommandJson(command.json());
throw new RollbackTransactionAsCommandIsNotApprovedByCheckerException(initialCommandSource);
commandSource.setTransactionId(command.getTransactionId());
// TODO: this should be removed together with lines 133-135
commandSource.setCommandJson(command.json()); // Set back CommandSource json data
throw new RollbackTransactionAsCommandIsNotApprovedByCheckerException(commandSource);
}
result.setRollbackTransaction(null);
result.setRollbackTransaction(null);
publishHookEvent(wrapper.entityName(), wrapper.actionName(), command, result);
return result;
}
private void storeCommandToIdempotentFilter(CommandSource savedCommandSource) {
private void storeCommandIdInContext(CommandSource savedCommandSource) {
if (savedCommandSource.getId() == null) {
throw new IllegalStateException("Command source not saved");
}
saveCommandToRequest(savedCommandSource);
}
private void saveCommandToRequest(CommandSource savedCommandSource) {
// Idempotency filters and retry need this
fineractRequestContextHolder.setAttribute(COMMAND_SOURCE_ID, savedCommandSource.getId());
}
private void publishHookErrorEvent(CommandWrapper wrapper, JsonCommand command, Throwable t) {
ErrorInfo ex = commandSourceService.generateErrorException(t);
publishHookEvent(wrapper.entityName(), wrapper.actionName(), command, gson.toJson(ex));
private void publishHookErrorEvent(CommandWrapper wrapper, JsonCommand command, ErrorInfo errorInfo) {
publishHookEvent(wrapper.entityName(), wrapper.actionName(), command, gson.toJson(errorInfo));
}
private void exceptionWhenTheRequestAlreadyProcessed(CommandWrapper wrapper, String idempotencyKey) {
private void exceptionWhenTheRequestAlreadyProcessed(CommandWrapper wrapper, String idempotencyKey, boolean retry) {
CommandSource existingCommand = commandSourceService.findCommandSource(wrapper, idempotencyKey);
if (existingCommand != null) {
idempotentExceptionByStatus(UNDER_PROCESSING, existingCommand,
command -> new IdempotentCommandProcessUnderProcessingException(wrapper));
idempotentExceptionByStatus(ERROR, existingCommand, command -> new IdempotentCommandProcessFailedException(wrapper, command));
if (!retry) {
idempotentExceptionByStatus(ERROR, existingCommand,
command -> new IdempotentCommandProcessFailedException(wrapper, command));
}
idempotentExceptionByStatus(PROCESSED, existingCommand,
command -> new IdempotentCommandProcessSucceedException(wrapper, command.getResult(), command.getResultStatusCode()));
}
@@ -192,24 +200,23 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
@Transactional
@Override
public CommandProcessingResult logCommand(CommandSource commandSourceResult) {
commandSourceResult.markAsAwaitingApproval();
if (commandSourceResult.getIdempotencyKey() == null) {
commandSourceResult.setIdempotencyKey(idempotencyKeyGenerator.create());
public CommandProcessingResult logCommand(CommandSource commandSource) {
commandSource.markAsAwaitingApproval();
if (commandSource.getIdempotencyKey() == null) {
commandSource.setIdempotencyKey(idempotencyKeyGenerator.create());
}
commandSourceResult = commandSourceService.saveResultNoTransaction(commandSourceResult);
commandSource = commandSourceService.saveResultSameTransaction(commandSource);
return new CommandProcessingResultBuilder().withCommandId(commandSourceResult.getId())
.withEntityId(commandSourceResult.getResourceId()).build();
return new CommandProcessingResultBuilder().withCommandId(commandSource.getId()).withEntityId(commandSource.getResourceId())
.build();
}
@SuppressWarnings("unused")
private CommandProcessingResult fallbackExecuteCommand(Exception e) throws Exception {
private CommandProcessingResult fallbackExecuteCommand(Exception e) {
if (e instanceof RollbackTransactionAsCommandIsNotApprovedByCheckerException ex) {
return logCommand(ex.getCommandSourceResult());
}
throw e;
throw errorHandler.getMappable(e);
}
private NewCommandSourceHandler findCommandHandler(final CommandWrapper wrapper) {
@@ -278,7 +285,6 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
}
private void publishHookEvent(final String entityName, final String actionName, JsonCommand command, final Object result) {
try {
final AppUser appUser = context.authenticatedUser(CommandWrapper.wrap(actionName, entityName, null, null));
@@ -18,16 +18,26 @@
*/
package org.apache.fineract.infrastructure.core.data;
import static lombok.AccessLevel.PROTECTED;
import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
import static org.apache.http.HttpStatus.SC_CONFLICT;
import static org.apache.http.HttpStatus.SC_FORBIDDEN;
import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
import static org.apache.http.HttpStatus.SC_METHOD_NOT_ALLOWED;
import static org.apache.http.HttpStatus.SC_NOT_FOUND;
import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE;
import static org.apache.http.HttpStatus.SC_UNAUTHORIZED;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.gson.Gson;
import jakarta.ws.rs.core.Response.Status;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
/**
*
*/
@Getter
@Setter(PROTECTED)
public class ApiGlobalErrorResponse {
/**
@@ -58,176 +68,125 @@ public class ApiGlobalErrorResponse {
private List<ApiParameterError> errors = new ArrayList<>();
protected ApiGlobalErrorResponse() {}
public static ApiGlobalErrorResponse create(int statusCode, String msgCode, String developerMessage, String defaultUserMessage,
List<ApiParameterError> errors) {
ApiGlobalErrorResponse response = new ApiGlobalErrorResponse();
response.setHttpStatusCode(String.valueOf(statusCode));
response.setUserMessageGlobalisationCode(msgCode);
response.setDeveloperMessage(developerMessage);
response.setDefaultUserMessage(defaultUserMessage);
response.setErrors(errors);
return response;
}
public static ApiGlobalErrorResponse create(int statusCode, String msgCode, String developerMessage, String defaultUserMessage) {
return create(statusCode, msgCode, developerMessage, defaultUserMessage, null);
}
public static ApiGlobalErrorResponse unAuthenticated() {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("401");
globalErrorResponse.setDeveloperMessage("Invalid authentication details were passed in api request.");
globalErrorResponse.setUserMessageGlobalisationCode("error.msg.not.authenticated");
globalErrorResponse.setDefaultUserMessage("Unauthenticated. Please login.");
return globalErrorResponse;
return create(SC_UNAUTHORIZED, "error.msg.not.authenticated", "Invalid authentication details were passed in api request.",
"Unauthenticated. Please login.");
}
public static ApiGlobalErrorResponse invalidTenantIdentifier() {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("401");
globalErrorResponse.setDeveloperMessage("Invalid tenant details were passed in api request.");
globalErrorResponse.setUserMessageGlobalisationCode("error.msg.invalid.tenant.identifier");
globalErrorResponse.setDefaultUserMessage("Invalid tenant identifier provided with request.");
return globalErrorResponse;
return create(SC_UNAUTHORIZED, "error.msg.invalid.tenant.identifier", "Invalid tenant details were passed in api request.",
"Invalid tenant identifier provided with request.");
}
public static ApiGlobalErrorResponse invalidInstanceTypeMethod(final String method) {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode(Status.METHOD_NOT_ALLOWED.toString());
globalErrorResponse.setDeveloperMessage("Invalid instance type called in api request for the method " + method);
globalErrorResponse.setUserMessageGlobalisationCode("error.msg.invalid.instance.type");
globalErrorResponse.setDefaultUserMessage("Invalid method " + method + " used with request to this instance type.");
return globalErrorResponse;
return create(SC_METHOD_NOT_ALLOWED, "error.msg.invalid.instance.type",
"Invalid instance type called in api request for the method " + method,
"Invalid method " + method + " used with request to this instance type.");
}
public static ApiGlobalErrorResponse loanIsLocked(final Long loanId) {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode(Status.CONFLICT.toString());
globalErrorResponse.setDeveloperMessage("Loan is locked by the COB job. Loan ID: " + loanId);
globalErrorResponse.setUserMessageGlobalisationCode("error.msg.loan.locked");
globalErrorResponse.setDefaultUserMessage("Loan is locked by the COB job. Loan ID: \" + loanId");
String msg = "Loan is locked by the COB job. Loan ID: " + loanId;
return create(SC_CONFLICT, "error.msg.loan.locked", msg, msg);
}
return globalErrorResponse;
public static ApiGlobalErrorResponse conflict(String type, String identifier) {
String details = "";
if (type == null) {
type = "unknown";
} else {
details = " on " + type;
}
if (identifier != null) {
details += " [" + identifier + ']';
}
String msg = "The server is currently unable to handle the request due to concurrent modification" + details + ", please try again";
return create(SC_CONFLICT, "error.msg.platform.service." + type + ".conflict", msg, msg);
}
public static ApiGlobalErrorResponse unAuthorized(final String defaultUserMessage) {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("403");
globalErrorResponse.setDeveloperMessage(
"The user associated with credentials passed on this request does not have sufficient privileges to perform this action.");
globalErrorResponse.setUserMessageGlobalisationCode("error.msg.not.authorized");
globalErrorResponse.setDefaultUserMessage("Insufficient privileges to perform this action.");
String msgCode = "error.msg.not.authorized";
final List<ApiParameterError> errors = List.of(ApiParameterError.generalError(msgCode, defaultUserMessage));
final List<ApiParameterError> errors = new ArrayList<>();
errors.add(ApiParameterError.generalError("error.msg.not.authorized", defaultUserMessage));
globalErrorResponse.setErrors(errors);
return globalErrorResponse;
return create(SC_FORBIDDEN, msgCode,
"The user associated with credentials passed on this request does not have sufficient privileges to perform this action.",
"Insufficient privileges to perform this action.", errors);
}
public static ApiGlobalErrorResponse domainRuleViolation(final String globalisationMessageCode, final String defaultUserMessage,
final Object... defaultUserMessageArgs) {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("403");
globalErrorResponse.setDeveloperMessage("Request was understood but caused a domain rule violation.");
globalErrorResponse.setUserMessageGlobalisationCode("validation.msg.domain.rule.violation");
globalErrorResponse.setDefaultUserMessage("Errors contain reason for domain rule violation.");
final List<ApiParameterError> errors = new ArrayList<>();
errors.add(ApiParameterError.generalError(globalisationMessageCode, defaultUserMessage, defaultUserMessageArgs));
globalErrorResponse.setErrors(errors);
return globalErrorResponse;
}
public static ApiGlobalErrorResponse notFound(final String globalisationMessageCode, final String defaultUserMessage,
final Object... defaultUserMessageArgs) {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("404");
globalErrorResponse.setDeveloperMessage("The requested resource is not available.");
globalErrorResponse.setUserMessageGlobalisationCode("error.msg.resource.not.found");
globalErrorResponse.setDefaultUserMessage("The requested resource is not available.");
final List<ApiParameterError> errors = new ArrayList<>();
errors.add(ApiParameterError.resourceIdentifierNotFound(globalisationMessageCode, defaultUserMessage, defaultUserMessageArgs));
globalErrorResponse.setErrors(errors);
return globalErrorResponse;
return create(SC_FORBIDDEN, "validation.msg.domain.rule.violation", "Request was understood but caused a domain rule violation.",
"Errors contain reason for domain rule violation.", errors);
}
public static ApiGlobalErrorResponse dataIntegrityError(final String globalisationMessageCode, final String defaultUserMessage,
final String parameterName, final Object... defaultUserMessageArgs) {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("403");
globalErrorResponse.setDeveloperMessage("The request caused a data integrity issue to be fired by the database.");
globalErrorResponse.setUserMessageGlobalisationCode(globalisationMessageCode);
globalErrorResponse.setDefaultUserMessage(defaultUserMessage);
final List<ApiParameterError> errors = new ArrayList<>();
errors.add(ApiParameterError.parameterError(globalisationMessageCode, defaultUserMessage, parameterName, defaultUserMessageArgs));
globalErrorResponse.setErrors(errors);
return globalErrorResponse;
return create(SC_FORBIDDEN, globalisationMessageCode, "The request caused a data integrity issue to be fired by the database.",
defaultUserMessage, errors);
}
public static ApiGlobalErrorResponse notFound(final String globalisationMessageCode, final String defaultUserMessage,
final Object... defaultUserMessageArgs) {
String msg = "The requested resource is not available.";
final List<ApiParameterError> errors = new ArrayList<>();
errors.add(ApiParameterError.resourceIdentifierNotFound(globalisationMessageCode, defaultUserMessage, defaultUserMessageArgs));
return create(SC_NOT_FOUND, "error.msg.resource.not.found", msg, msg, errors);
}
public static ApiGlobalErrorResponse badClientRequest(final String globalisationMessageCode, final String defaultUserMessage,
final List<ApiParameterError> errors) {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("400");
globalErrorResponse
.setDeveloperMessage("The request was invalid. This typically will happen due to validation errors which are provided.");
globalErrorResponse.setUserMessageGlobalisationCode(globalisationMessageCode);
globalErrorResponse.setDefaultUserMessage(defaultUserMessage);
globalErrorResponse.setErrors(errors);
return globalErrorResponse;
}
public static ApiGlobalErrorResponse jobIsDisabled(final String globalisationMessageCode, final String defaultUserMessage) {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode(Status.FORBIDDEN.toString());
globalErrorResponse.setDeveloperMessage(defaultUserMessage);
globalErrorResponse.setUserMessageGlobalisationCode(globalisationMessageCode);
globalErrorResponse.setDefaultUserMessage(defaultUserMessage);
return globalErrorResponse;
return create(SC_BAD_REQUEST, globalisationMessageCode,
"The request was invalid. This typically will happen due to validation errors which are provided.", defaultUserMessage,
errors);
}
public static ApiGlobalErrorResponse badClientRequest(final String globalisationMessageCode, final String defaultUserMessage) {
return badClientRequest(globalisationMessageCode, defaultUserMessage, Collections.emptyList());
}
public static ApiGlobalErrorResponse jobIsDisabled(final String globalisationMessageCode, final String defaultUserMessage) {
return create(SC_FORBIDDEN, globalisationMessageCode, defaultUserMessage, defaultUserMessage);
}
public static ApiGlobalErrorResponse serverSideError(final String globalisationMessageCode, final String defaultUserMessage,
final Object... defaultUserMessageArgs) {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("500");
globalErrorResponse.setDeveloperMessage("An unexpected error occured on the platform server.");
globalErrorResponse.setUserMessageGlobalisationCode("error.msg.platform.server.side.error");
globalErrorResponse.setDefaultUserMessage("An unexpected error occured on the platform server.");
String msg = "An unexpected error occured on the platform server.";
final List<ApiParameterError> errors = new ArrayList<>();
errors.add(ApiParameterError.generalError(globalisationMessageCode, defaultUserMessage, defaultUserMessageArgs));
globalErrorResponse.setErrors(errors);
return globalErrorResponse;
return create(SC_INTERNAL_SERVER_ERROR, "error.msg.platform.server.side.error", msg, msg, errors);
}
public static ApiGlobalErrorResponse serviceUnavailable(final String globalisationMessageCode, final String defaultUserMessage,
final Object... defaultUserMessageArgs) {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("503");
globalErrorResponse.setDeveloperMessage("The server is currently unable to handle the request , please try after some time.");
globalErrorResponse.setUserMessageGlobalisationCode("error.msg.platform.service.unavailable");
globalErrorResponse.setDefaultUserMessage("The server is currently unable to handle the request , please try after some time.");
String msg = "The server is currently unable to handle the request , please try after some time.";
final List<ApiParameterError> errors = new ArrayList<>();
errors.add(ApiParameterError.generalError(globalisationMessageCode, defaultUserMessage, defaultUserMessageArgs));
globalErrorResponse.setErrors(errors);
return globalErrorResponse;
}
protected ApiGlobalErrorResponse() {}
public ApiGlobalErrorResponse(final List<ApiParameterError> errors) {
this.errors = errors;
return create(SC_SERVICE_UNAVAILABLE, "error.msg.platform.service.unavailable", msg, msg, errors);
}
@JsonProperty("errors")
@@ -235,42 +194,6 @@ public class ApiGlobalErrorResponse {
return this.errors;
}
public void setErrors(final List<ApiParameterError> errors) {
this.errors = errors;
}
public String getDeveloperMessage() {
return this.developerMessage;
}
public void setDeveloperMessage(final String developerMessage) {
this.developerMessage = developerMessage;
}
public String getHttpStatusCode() {
return this.httpStatusCode;
}
public void setHttpStatusCode(final String httpStatusCode) {
this.httpStatusCode = httpStatusCode;
}
public String getDefaultUserMessage() {
return this.defaultUserMessage;
}
public void setDefaultUserMessage(final String defaultUserMessage) {
this.defaultUserMessage = defaultUserMessage;
}
public String getUserMessageGlobalisationCode() {
return this.userMessageGlobalisationCode;
}
public void setUserMessageGlobalisationCode(final String userMessageGlobalisationCode) {
this.userMessageGlobalisationCode = userMessageGlobalisationCode;
}
public String toJson() {
final Gson gson = new Gson();
return gson.toJson(this);
@@ -19,7 +19,6 @@
package org.apache.fineract.infrastructure.core.data;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import lombok.Getter;
import lombok.ToString;
@@ -51,12 +50,43 @@ public class CommandProcessingResult implements Serializable {
private final ExternalId resourceExternalId;
private final ExternalId subResourceExternalId;
public static CommandProcessingResult fromCommandProcessingResult(CommandProcessingResult commandResult) {
return new CommandProcessingResult(commandResult.commandId, commandResult.officeId, commandResult.groupId, commandResult.clientId,
commandResult.loanId, commandResult.savingsId, commandResult.resourceIdentifier, commandResult.resourceId,
commandResult.transactionId, commandResult.changes, commandResult.productId, commandResult.gsimId, commandResult.glimId,
commandResult.creditBureauReportData, commandResult.rollbackTransaction, commandResult.subResourceId,
commandResult.resourceExternalId, commandResult.subResourceExternalId);
private CommandProcessingResult(final Long commandId, final Long officeId, final Long groupId, final Long clientId, final Long loanId,
final Long savingsId, final String resourceIdentifier, final Long resourceId, final String transactionId,
final Map<String, Object> changes, final Long productId, final Long gsimId, final Long glimId,
final Map<String, Object> creditBureauReportData, Boolean rollbackTransaction, final Long subResourceId,
final ExternalId resourceExternalId, final ExternalId subResourceExternalId) {
this.commandId = commandId;
this.officeId = officeId;
this.groupId = groupId;
this.clientId = clientId;
this.loanId = loanId;
this.savingsId = savingsId;
this.resourceIdentifier = resourceIdentifier;
this.resourceId = resourceId;
this.changes = changes;
this.transactionId = transactionId;
this.productId = productId;
this.gsimId = gsimId;
this.glimId = glimId;
this.creditBureauReportData = creditBureauReportData;
this.rollbackTransaction = rollbackTransaction;
this.subResourceId = subResourceId;
this.resourceExternalId = resourceExternalId;
this.subResourceExternalId = subResourceExternalId;
}
protected CommandProcessingResult(final Long resourceId, final Long officeId, final Long commandId, final Map<String, Object> changes,
Long clientId) {
this(commandId, officeId, null, clientId, null, null, resourceId == null ? null : resourceId.toString(), resourceId, null, changes,
null, null, null, null, null, null, ExternalId.empty(), ExternalId.empty());
}
protected CommandProcessingResult(final Long resourceId, final Long officeId, final Long commandId, final Map<String, Object> changes) {
this(resourceId, officeId, commandId, changes, null);
}
protected CommandProcessingResult(final Long resourceId) {
this(resourceId, null, null, null);
}
public static CommandProcessingResult fromCommandProcessingResult(CommandProcessingResult commandResult, final Long resourceId) {
@@ -67,6 +97,10 @@ public class CommandProcessingResult implements Serializable {
commandResult.resourceExternalId, commandResult.subResourceExternalId);
}
public static CommandProcessingResult fromCommandProcessingResult(CommandProcessingResult commandResult) {
return fromCommandProcessingResult(commandResult, commandResult.getResourceId());
}
public static CommandProcessingResult fromDetails(final Long commandId, final Long officeId, final Long groupId, final Long clientId,
final Long loanId, final Long savingsId, final String resourceIdentifier, final Long entityId, final Long gsimId,
final Long glimId, final Map<String, Object> creditBureauReportData, final String transactionId,
@@ -81,21 +115,16 @@ public class CommandProcessingResult implements Serializable {
return new CommandProcessingResult(null, null, commandId, null);
}
public static CommandProcessingResult resourceResult(final Long resourceId, final Long commandId) {
return new CommandProcessingResult(resourceId, null, commandId, null);
}
public static CommandProcessingResult resourceResult(final Long resourceId, final Long commandId, final Map<String, Object> changes) {
return new CommandProcessingResult(resourceId, null, commandId, changes);
}
public static CommandProcessingResult subResourceResult(final Long resourceId, final Long subResourceId, final Long commandId) {
return new CommandProcessingResult(resourceId, subResourceId, commandId, null);
public static CommandProcessingResult resourceResult(final Long resourceId, final Long commandId) {
return new CommandProcessingResult(resourceId, null, commandId, null);
}
public static CommandProcessingResult subResourceResult(final Long resourceId, final Long subResourceId, final Long commandId,
final Map<String, Object> changes) {
return new CommandProcessingResult(resourceId, subResourceId, commandId, changes);
public static CommandProcessingResult resourceResult(final Long resourceId) {
return new CommandProcessingResult(resourceId);
}
public static CommandProcessingResult withChanges(final Long resourceId, final Map<String, Object> changes) {
@@ -106,88 +135,6 @@ public class CommandProcessingResult implements Serializable {
return new CommandProcessingResult(null, null, null, null);
}
/*
* Deprecated
*/
public CommandProcessingResult(final Long entityId) {
if (entityId != null) {
this.resourceIdentifier = entityId.toString();
} else {
this.resourceIdentifier = null;
}
this.resourceId = entityId;
this.officeId = null;
this.groupId = null;
this.clientId = null;
this.loanId = null;
this.savingsId = null;
this.transactionId = null;
this.changes = new HashMap<>();
this.productId = null;
this.gsimId = null;
this.glimId = null;
this.creditBureauReportData = null;
this.subResourceId = null;
this.resourceExternalId = ExternalId.empty();
this.subResourceExternalId = ExternalId.empty();
}
private CommandProcessingResult(final Long commandId, final Long officeId, final Long groupId, final Long clientId, final Long loanId,
final Long savingsId, final String resourceIdentifier, final Long resourceId, final String transactionId,
final Map<String, Object> changesOnly, final Long productId, final Long gsimId, final Long glimId,
final Map<String, Object> creditBureauReportData, Boolean rollbackTransaction, final Long subResourceId,
final ExternalId resourceExternalId, final ExternalId subResourceExternalId) {
this.commandId = commandId;
this.officeId = officeId;
this.groupId = groupId;
this.clientId = clientId;
this.loanId = loanId;
this.savingsId = savingsId;
this.resourceIdentifier = resourceIdentifier;
this.resourceId = resourceId;
this.changes = changesOnly;
this.transactionId = transactionId;
this.productId = productId;
this.gsimId = gsimId;
this.glimId = glimId;
this.creditBureauReportData = creditBureauReportData;
this.rollbackTransaction = rollbackTransaction;
this.subResourceId = subResourceId;
this.resourceExternalId = resourceExternalId;
this.subResourceExternalId = subResourceExternalId;
}
protected CommandProcessingResult(final Long resourceId, final Long officeId, final Long commandId,
final Map<String, Object> changesOnly) {
if (resourceId != null) {
this.resourceIdentifier = resourceId.toString();
} else {
this.resourceIdentifier = null;
}
this.resourceId = resourceId;
this.officeId = officeId;
this.groupId = null;
this.clientId = null;
this.loanId = null;
this.savingsId = null;
this.transactionId = null;
this.commandId = commandId;
this.changes = changesOnly;
this.productId = null;
this.gsimId = null;
this.glimId = null;
this.creditBureauReportData = null;
this.subResourceId = null;
this.resourceExternalId = ExternalId.empty();
this.subResourceExternalId = ExternalId.empty();
}
protected CommandProcessingResult(final Long resourceId, final Long officeId, final Long commandId,
final Map<String, Object> changesOnly, long clientId) {
this(commandId, officeId, null, clientId, null, null, null, resourceId, null, changesOnly, null, null, null, null, null, null,
ExternalId.empty(), ExternalId.empty());
}
public void setOfficeId(final Long officeId) {
this.officeId = officeId;
}
@@ -0,0 +1,60 @@
/**
* 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.exceptionmapper;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
import org.springframework.context.annotation.Scope;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Component;
/**
* An {@link ExceptionMapper} to map {@link ObjectOptimisticLockingFailureException} thrown by platform into a HTTP API
* friendly format.
*/
@Provider
@Component
@Scope("singleton")
@Slf4j
public class ConcurrencyFailureExceptionMapper implements FineractExceptionMapper, ExceptionMapper<ConcurrencyFailureException> {
@Override
public Response toResponse(final ConcurrencyFailureException exception) {
log.warn("Exception: {}, Message: {}", exception.getClass().getName(), exception.getMessage());
String type = "unknown";
String identifier = "unknown";
if (exception instanceof ObjectOptimisticLockingFailureException olex) {
type = olex.getPersistentClassName();
identifier = olex.getIdentifier() == null ? null : String.valueOf(olex.getIdentifier());
}
final ApiGlobalErrorResponse dataIntegrityError = ApiGlobalErrorResponse.conflict(type, identifier);
return Response.status(Status.CONFLICT).entity(dataIntegrityError).type(MediaType.APPLICATION_JSON).build();
}
@Override
public int errorCode() {
return 4009;
}
}
@@ -0,0 +1,68 @@
/**
* 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.exceptionmapper;
import static org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import lombok.extern.slf4j.Slf4j;
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.eclipse.persistence.exceptions.OptimisticLockException;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
/**
* An {@link ExceptionMapper} to map {@link OptimisticLockException} thrown by platform into a HTTP API friendly format.
*/
@Provider
@Component
@Scope("singleton")
@Slf4j
public class IdempotentCommandExceptionMapper implements FineractExceptionMapper, ExceptionMapper<AbstractIdempotentCommandException> {
@Override
public Response toResponse(final AbstractIdempotentCommandException exception) {
log.warn("Processing {} request: {}", exception.getClass().getName(), exception.getMessage());
Status status = null;
if (exception instanceof IdempotentCommandProcessSucceedException pse) {
status = Status.fromStatusCode(pse.getStatusCode());
} else if (exception instanceof IdempotentCommandProcessUnderProcessingException) {
status = Status.CONFLICT;
} else if (exception instanceof IdempotentCommandProcessFailedException pfe) {
status = Status.fromStatusCode(pfe.getStatusCode());
}
if (status == null) {
status = Status.INTERNAL_SERVER_ERROR;
}
return Response.status(status).entity(exception.getResponse()).header(IDEMPOTENT_CACHE_HEADER, "true")
.type(MediaType.APPLICATION_JSON).build();
}
@Override
public int errorCode() {
return 4209;
}
}
@@ -1,45 +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.core.exceptionmapper;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException;
import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessSucceedException;
import org.springframework.stereotype.Component;
@Provider
@Component
@Slf4j
public class IdempotentCommandProcessSucceedExceptionMapper implements ExceptionMapper<IdempotentCommandProcessSucceedException> {
@Override
public Response toResponse(final IdempotentCommandProcessSucceedException exception) {
log.debug("Idempotent processing success request: {}", exception.getMessage());
Response.ResponseBuilder responseBuilder = Response.status(exception.getStatusCode());
if (exception.getResponse() != null) {
responseBuilder.entity(exception.getResponse());
}
return responseBuilder.header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER, "true").type(MediaType.APPLICATION_JSON)
.build();
}
}
@@ -1,44 +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.core.exceptionmapper;
import static org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessUnderProcessingException;
import org.springframework.stereotype.Component;
@Provider
@Component
@Slf4j
public class IdempotentCommandProcessUnderProcessingExceptionMapper
implements ExceptionMapper<IdempotentCommandProcessUnderProcessingException> {
@Override
public Response toResponse(final IdempotentCommandProcessUnderProcessingException exception) {
log.debug("Idempotent under processing request: {}", exception.getMessage());
return Response.status(Status.CONFLICT).entity(exception.getResponse()).header(IDEMPOTENT_CACHE_HEADER, "true")
.type(MediaType.APPLICATION_JSON).build();
}
}
@@ -24,20 +24,33 @@ import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException;
import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessFailedException;
import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
import org.eclipse.persistence.exceptions.OptimisticLockException;
import org.springframework.context.annotation.Scope;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Component;
/**
* An {@link ExceptionMapper} to map {@link ObjectOptimisticLockingFailureException} thrown by platform into a HTTP API
* friendly format.
*/
@Provider
@Component
@Scope("singleton")
@Slf4j
public class IdempotentCommandProcessFailedExceptionMapper implements ExceptionMapper<IdempotentCommandProcessFailedException> {
public class OptimisticLockExceptionMapper implements FineractExceptionMapper, ExceptionMapper<OptimisticLockException> {
@Override
public Response toResponse(final IdempotentCommandProcessFailedException exception) {
log.debug("Idempotent processing failed request: {}", exception.getMessage());
Status statusCode = Status.fromStatusCode(exception.getStatusCode());
return Response.status(statusCode).entity(exception.getResponse())
.header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER, "true").type(MediaType.APPLICATION_JSON).build();
public Response toResponse(final OptimisticLockException exception) {
log.warn("Exception: {}, Message: {}", exception.getClass().getName(), exception.getMessage());
String type = exception.getQuery() == null ? "unknown" : "query";
String identifier = "unknown";
final ApiGlobalErrorResponse dataIntegrityError = ApiGlobalErrorResponse.conflict(type, identifier);
return Response.status(Status.CONFLICT).entity(dataIntegrityError).type(MediaType.APPLICATION_JSON).build();
}
@Override
public int errorCode() {
return 4009;
}
}
@@ -43,9 +43,9 @@ public class IdempotencyStoreBatchFilter implements BatchFilter {
.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();
boolean isSuccessWithoutStored = commandId.isPresent() && helper.isStoreIdempotencyKey(null);
if (isSuccessWithoutStored) {
helper.storeCommandResult(true, result.getStatusCode(), result.getBody(), commandId);
helper.storeCommandResult(result.getStatusCode(), result.getBody(), commandId.get());
}
return result;
}
@@ -56,12 +56,12 @@ public class IdempotencyStoreFilter extends OncePerRequestFilter {
filterChain.doFilter(request, wrapper.getValue() != null ? wrapper.getValue() : response);
Optional<Long> commandId = helper.getCommandId(request);
boolean isSuccessWithoutStored = helper.isStoreIdempotencyKey(request) && commandId.isPresent()
&& helper.isAllowedContentTypeResponse(response) && wrapper.getValue() != null;
boolean isSuccessWithoutStored = commandId.isPresent() && wrapper.getValue() != null && helper.isStoreIdempotencyKey(request)
&& helper.isAllowedContentTypeResponse(response);
if (isSuccessWithoutStored) {
helper.storeCommandResult(false, response.getStatus(), Optional.ofNullable(wrapper.getValue())
helper.storeCommandResult(response.getStatus(), Optional.ofNullable(wrapper.getValue())
.map(ContentCachingResponseWrapper::getContentAsByteArray).map(s -> new String(s, StandardCharsets.UTF_8)).orElse(null),
commandId);
commandId.get());
}
if (wrapper.getValue() != null) {
wrapper.getValue().copyBodyToResponse();
@@ -25,6 +25,7 @@ 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.BatchRequestContextHolder;
import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
import org.springframework.stereotype.Component;
@@ -36,15 +37,13 @@ public class IdempotencyStoreHelper {
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 -> {
public void storeCommandResult(Integer response, String body, Long commandId) {
commandSourceRepository.findById(commandId).ifPresent(commandSource -> {
boolean sameTransaction = BatchRequestContextHolder.getEnclosingTransaction().isPresent();
commandSource.setResultStatusCode(response);
commandSource.setResult(body);
if (batch) {
commandSourceService.saveResultNoTransaction(commandSource);
} else {
commandSourceService.saveResult(commandSource);
}
commandSource = sameTransaction ? commandSourceService.saveResultSameTransaction(commandSource)
: commandSourceService.saveResultNewTransaction(commandSource);
});
}
@@ -32,7 +32,7 @@ import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.ActionContext;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.exceptionmapper.IdempotentCommandProcessFailedExceptionMapper;
import org.apache.fineract.infrastructure.core.exceptionmapper.IdempotentCommandExceptionMapper;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -50,7 +50,7 @@ public class IdempotencyCommandProcessFailedExceptionTest {
@Test
public void testInconsistentStatus() {
IdempotentCommandProcessFailedExceptionMapper mapper = new IdempotentCommandProcessFailedExceptionMapper();
IdempotentCommandExceptionMapper mapper = new IdempotentCommandExceptionMapper();
CommandWrapper command = new CommandWrapper(null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null);
CommandSource source = CommandSource.fullEntryFrom(command, JsonCommand.from("{}"), null, "dummy-key", null);