FINERACT-1724

- Error handler is springified, exception mappers are retrieved as spring beans
 - backward compatibility is maintained
This commit is contained in:
Peter Bagrij
2023-07-10 16:56:12 +02:00
committed by Adam Saghy
parent 749a77efac
commit 2afd9d35e7
21 changed files with 604 additions and 101 deletions
@@ -18,29 +18,21 @@
*/
package org.apache.fineract.batch.exception;
import static org.springframework.core.ResolvableType.forClassWithGenerics;
import com.google.gson.Gson;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.exception.PlatformInternalServerException;
import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException;
import org.apache.fineract.infrastructure.core.exceptionmapper.PlatformApiDataValidationExceptionMapper;
import org.apache.fineract.infrastructure.core.exceptionmapper.PlatformDataIntegrityExceptionMapper;
import org.apache.fineract.infrastructure.core.exceptionmapper.PlatformDomainRuleExceptionMapper;
import org.apache.fineract.infrastructure.core.exceptionmapper.PlatformInternalServerExceptionMapper;
import org.apache.fineract.infrastructure.core.exceptionmapper.PlatformResourceNotFoundExceptionMapper;
import org.apache.fineract.infrastructure.core.exceptionmapper.UnsupportedParameterExceptionMapper;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.SetUtils;
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.apache.http.HttpStatus;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.transaction.TransactionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* Provides an Error Handler method that returns an object of type {@link ErrorInfo} to the CommandStrategy which raised
@@ -50,60 +42,41 @@ import org.springframework.transaction.TransactionException;
* @author Rishabh Shukla
* @see org.apache.fineract.batch.command.CommandStrategy
*/
@Component
@Slf4j
@AllArgsConstructor
public final class ErrorHandler {
@Autowired
private final ApplicationContext ctx;
@Autowired
private final DefaultExceptionMapper defaultExceptionMapper;
private static final Gson JSON_HELPER = GoogleGsonSerializerHelper.createGsonBuilder(true).create();
private static final Map<Class<? extends Exception>, Function<RuntimeException, ErrorInfo>> EXCEPTION_HANDLERS = Collections
.synchronizedMap(new LinkedHashMap<>());
private static final Map.Entry<Class<? extends Exception>, Function<RuntimeException, ErrorInfo>> DEFAULT_ERROR_HANDLER = Map.entry(
RuntimeException.class,
runtimeException -> new ErrorInfo(500, 9999, "{\"Exception\": %s}".formatted(runtimeException.getMessage())));
static {
EXCEPTION_HANDLERS.put(AbstractPlatformResourceNotFoundException.class,
runtimeException -> handleException(runtimeException, new PlatformResourceNotFoundExceptionMapper(), 1001));
EXCEPTION_HANDLERS.put(UnsupportedParameterException.class,
runtimeException -> handleException(runtimeException, new UnsupportedParameterExceptionMapper(), 2001));
EXCEPTION_HANDLERS.put(PlatformApiDataValidationException.class,
runtimeException -> handleException(runtimeException, new PlatformApiDataValidationExceptionMapper(), 2002));
EXCEPTION_HANDLERS.put(PlatformDataIntegrityException.class,
runtimeException -> handleException(runtimeException, new PlatformDataIntegrityExceptionMapper(), 3001));
EXCEPTION_HANDLERS.put(AbstractPlatformDomainRuleException.class,
runtimeException -> handleException(runtimeException, new PlatformDomainRuleExceptionMapper(), 9999));
EXCEPTION_HANDLERS.put(TransactionException.class, runtimeException -> new ErrorInfo(HttpStatus.SC_INTERNAL_SERVER_ERROR, 4001,
"{\"Exception\": %s}".formatted(runtimeException.getMessage())));
EXCEPTION_HANDLERS.put(PlatformInternalServerException.class,
runtimeException -> handleException(runtimeException, new PlatformInternalServerExceptionMapper(), 5001));
EXCEPTION_HANDLERS.put(NonTransientDataAccessException.class, runtimeException -> new ErrorInfo(HttpStatus.SC_INTERNAL_SERVER_ERROR,
4002, "{\"Exception\": %s}".formatted(runtimeException.getMessage())));
EXCEPTION_HANDLERS.put(DEFAULT_ERROR_HANDLER.getKey(), DEFAULT_ERROR_HANDLER.getValue());
private <T extends RuntimeException> ExceptionMapper<T> findMostSpecificExceptionHandler(T exception) {
Class<?> clazz = exception.getClass();
do {
Set<String> exceptionMappers = createSet(ctx.getBeanNamesForType(forClassWithGenerics(ExceptionMapper.class, clazz)));
Set<String> fineractErrorMappers = createSet(ctx.getBeanNamesForType(FineractExceptionMapper.class));
SetUtils.SetView<String> intersection = SetUtils.intersection(exceptionMappers, fineractErrorMappers);
if (intersection.size() > 0) {
// noinspection unchecked
return (ExceptionMapper<T>) ctx.getBean(intersection.iterator().next());
}
clazz = clazz.getSuperclass();
} while (!clazz.equals(Exception.class));
// noinspection unchecked
return (ExceptionMapper<T>) defaultExceptionMapper;
}
private ErrorHandler() {}
public static void registerNewErrorHandler(final Class<? extends RuntimeException> exceptionClass, final ExceptionMapper mapper,
int errorCode) {
LinkedHashMap<Class<? extends Exception>, Function<RuntimeException, ErrorInfo>> newHandlers = new LinkedHashMap<>();
newHandlers.put(exceptionClass, runtimeException -> handleException(runtimeException, mapper, errorCode));
EXCEPTION_HANDLERS.forEach(newHandlers::putIfAbsent);
EXCEPTION_HANDLERS.clear();
newHandlers.forEach(EXCEPTION_HANDLERS::putIfAbsent);
}
public static void registerNewErrorHandler(final Class<? extends RuntimeException> exceptionClass,
Function<RuntimeException, ErrorInfo> function) {
LinkedHashMap<Class<? extends Exception>, Function<RuntimeException, ErrorInfo>> newHandlers = new LinkedHashMap<>();
newHandlers.put(exceptionClass, function);
EXCEPTION_HANDLERS.forEach(newHandlers::putIfAbsent);
EXCEPTION_HANDLERS.clear();
newHandlers.forEach(EXCEPTION_HANDLERS::putIfAbsent);
}
private static ErrorInfo handleException(final RuntimeException exception, final ExceptionMapper mapper, final int errorCode) {
final Response response = mapper.toResponse(exception);
final String errorBody = JSON_HELPER.toJson(response.getEntity());
return new ErrorInfo(response.getStatus(), errorCode, errorBody);
private <T> Set<T> createSet(T[] array) {
if (array == null) {
return Set.of();
} else {
return Set.of(array);
}
}
/**
@@ -112,9 +85,10 @@ public final class ErrorHandler {
* @param exception
* @return ErrorInfo
*/
public static ErrorInfo handler(final RuntimeException exception) {
Map.Entry<Class<? extends Exception>, Function<RuntimeException, ErrorInfo>> errorhandler = EXCEPTION_HANDLERS.entrySet().stream()
.filter(e -> e.getKey().isAssignableFrom(exception.getClass())).findFirst().orElse(DEFAULT_ERROR_HANDLER);
return errorhandler.getValue().apply(exception);
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()));
}
}
@@ -82,6 +82,7 @@ public class BatchApiServiceImpl implements BatchApiService {
private final CommandStrategyProvider strategyProvider;
private final ResolutionHelper resolutionHelper;
private final PlatformTransactionManager transactionManager;
private final ErrorHandler errorHandler;
private final List<BatchFilter> batchFilters;
@@ -106,7 +107,7 @@ public class BatchApiServiceImpl implements BatchApiService {
final List<BatchRequestNode> batchRequestNodes = this.resolutionHelper.getDependingRequests(requestList);
if (batchRequestNodes.isEmpty()) {
final BatchResponse response = new BatchResponse();
ErrorInfo ex = ErrorHandler.handler(new ClientDetailsNotFoundException());
ErrorInfo ex = errorHandler.handle(new ClientDetailsNotFoundException());
response.setStatusCode(500);
response.setBody(ex.getMessage());
responseList.add(response);
@@ -156,7 +157,7 @@ public class BatchApiServiceImpl implements BatchApiService {
try {
resolvedChildRequest = this.resolutionHelper.resoluteRequest(childNode.getRequest(), response);
} catch (RuntimeException ex) {
throw new BatchExecutionException(childNode.getRequest(), ex);
throw new BatchExecutionException(childNode.getRequest(), ex, errorHandler.handle(ex));
}
callRequestRecursive(resolvedChildRequest, childNode, responseList, uriInfo, enclosingTransaction);
@@ -206,7 +207,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());
throw new BatchExecutionException(request, preprocessorResult.getLeft(), errorHandler.handle(preprocessorResult.getLeft()));
} else {
request = preprocessorResult.get();
}
@@ -231,7 +232,7 @@ public class BatchApiServiceImpl implements BatchApiService {
} catch (AbstractIdempotentCommandException idempotentException) {
return handleIdempotentRequests(idempotentException, request);
} catch (RuntimeException ex) {
throw new BatchExecutionException(request, ex);
throw new BatchExecutionException(request, ex, errorHandler.handle(ex));
} finally {
BatchRequestContextHolder.resetRequestAttributes();
}
@@ -366,7 +367,7 @@ public class BatchApiServiceImpl implements BatchApiService {
return Arrays.asList(errResponse);
} catch (RuntimeException ex) {
status.setRollbackOnly();
ErrorInfo e = ErrorHandler.handler(ex);
ErrorInfo e = errorHandler.handle(ex);
BatchResponse errResponse = new BatchResponse();
errResponse.setStatusCode(e.getStatusCode());
errResponse.setBody(e.getMessage());
@@ -374,7 +375,7 @@ public class BatchApiServiceImpl implements BatchApiService {
}
});
} catch (TransactionException | NonTransientDataAccessException ex) {
ErrorInfo e = ErrorHandler.handler(ex);
ErrorInfo e = errorHandler.handle(ex);
return createErrorResponse(responseList, e.getStatusCode());
}
}
@@ -20,7 +20,6 @@ package org.apache.fineract.batch.service;
import lombok.Getter;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.exception.ErrorHandler;
import org.apache.fineract.batch.exception.ErrorInfo;
@Getter
@@ -29,9 +28,9 @@ public class BatchExecutionException extends RuntimeException {
private final BatchRequest request;
private final ErrorInfo errorInfo;
public BatchExecutionException(BatchRequest request, RuntimeException ex) {
public BatchExecutionException(BatchRequest request, RuntimeException ex, ErrorInfo errorInfo) {
super("Error executing batch request: " + request, ex);
this.request = request;
this.errorInfo = ErrorHandler.handler(ex);
this.errorInfo = errorInfo;
}
}
@@ -46,6 +46,7 @@ import org.springframework.transaction.annotation.Transactional;
public class CommandSourceService {
private final CommandSourceRepository commandSourceRepository;
private final ErrorHandler errorHandler;
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ)
public CommandSource saveInitial(CommandWrapper wrapper, JsonCommand jsonCommand, AppUser maker, String idempotencyKey) {
@@ -83,7 +84,7 @@ public class CommandSourceService {
public ErrorInfo generateErrorException(Throwable t) {
if (t instanceof final RuntimeException e) {
return ErrorHandler.handler(e);
return errorHandler.handle(e);
} else {
return new ErrorInfo(500, 9999, "{\"Exception\": " + t.toString() + "}");
}
@@ -0,0 +1,46 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.core.exceptionmapper;
import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("singleton")
@Slf4j
public class DefaultExceptionMapper implements FineractExceptionMapper, ExceptionMapper<RuntimeException> {
@Override
public int errorCode() {
return 9999;
}
@Override
public Response toResponse(RuntimeException runtimeException) {
return Response.status(SC_INTERNAL_SERVER_ERROR).entity(Map.of("Exception", runtimeException.getMessage()))
.type(MediaType.APPLICATION_JSON).build();
}
}
@@ -0,0 +1,24 @@
/**
* 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;
public interface FineractExceptionMapper {
int errorCode();
}
@@ -40,7 +40,8 @@ import org.springframework.stereotype.Component;
@Component
@Scope("singleton")
@Slf4j
public class PlatformApiDataValidationExceptionMapper implements ExceptionMapper<PlatformApiDataValidationException> {
public class PlatformApiDataValidationExceptionMapper
implements FineractExceptionMapper, ExceptionMapper<PlatformApiDataValidationException> {
@Override
public Response toResponse(final PlatformApiDataValidationException exception) {
@@ -50,4 +51,9 @@ public class PlatformApiDataValidationExceptionMapper implements ExceptionMapper
return Response.status(Status.BAD_REQUEST).entity(dataValidationErrorResponse).type(MediaType.APPLICATION_JSON).build();
}
@Override
public int errorCode() {
return 2002;
}
}
@@ -40,7 +40,7 @@ import org.springframework.stereotype.Component;
@Component
@Scope("singleton")
@Slf4j
public class PlatformDataIntegrityExceptionMapper implements ExceptionMapper<PlatformDataIntegrityException> {
public class PlatformDataIntegrityExceptionMapper implements FineractExceptionMapper, ExceptionMapper<PlatformDataIntegrityException> {
@Override
public Response toResponse(final PlatformDataIntegrityException exception) {
@@ -50,4 +50,9 @@ public class PlatformDataIntegrityExceptionMapper implements ExceptionMapper<Pla
return Response.status(Status.FORBIDDEN).entity(dataIntegrityError).type(MediaType.APPLICATION_JSON).build();
}
@Override
public int errorCode() {
return 3001;
}
}
@@ -40,7 +40,7 @@ import org.springframework.stereotype.Component;
@Component
@Scope("singleton")
@Slf4j
public class PlatformDomainRuleExceptionMapper implements ExceptionMapper<AbstractPlatformDomainRuleException> {
public class PlatformDomainRuleExceptionMapper implements FineractExceptionMapper, ExceptionMapper<AbstractPlatformDomainRuleException> {
@Override
public Response toResponse(final AbstractPlatformDomainRuleException exception) {
@@ -51,4 +51,9 @@ public class PlatformDomainRuleExceptionMapper implements ExceptionMapper<Abstra
// domain/business logic
return Response.status(Status.FORBIDDEN).entity(notFoundErrorResponse).type(MediaType.APPLICATION_JSON).build();
}
@Override
public int errorCode() {
return 9999;
}
}
@@ -39,7 +39,7 @@ import org.springframework.stereotype.Component;
@Component
@Scope("singleton")
@Slf4j
public class PlatformInternalServerExceptionMapper implements ExceptionMapper<PlatformInternalServerException> {
public class PlatformInternalServerExceptionMapper implements FineractExceptionMapper, ExceptionMapper<PlatformInternalServerException> {
@Override
public Response toResponse(final PlatformInternalServerException exception) {
@@ -48,4 +48,9 @@ public class PlatformInternalServerExceptionMapper implements ExceptionMapper<Pl
exception.getDefaultUserMessage(), exception.getDefaultUserMessageArgs());
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(notFoundErrorResponse).type(MediaType.APPLICATION_JSON).build();
}
@Override
public int errorCode() {
return 5001;
}
}
@@ -40,7 +40,8 @@ import org.springframework.stereotype.Component;
@Component
@Scope("singleton")
@Slf4j
public class PlatformResourceNotFoundExceptionMapper implements ExceptionMapper<AbstractPlatformResourceNotFoundException> {
public class PlatformResourceNotFoundExceptionMapper
implements FineractExceptionMapper, ExceptionMapper<AbstractPlatformResourceNotFoundException> {
@Override
public Response toResponse(final AbstractPlatformResourceNotFoundException exception) {
@@ -49,4 +50,9 @@ public class PlatformResourceNotFoundExceptionMapper implements ExceptionMapper<
exception.getDefaultUserMessage(), exception.getDefaultUserMessageArgs());
return Response.status(Status.NOT_FOUND).entity(notFoundErrorResponse).type(MediaType.APPLICATION_JSON).build();
}
@Override
public int errorCode() {
return 1001;
}
}
@@ -40,7 +40,7 @@ import org.springframework.stereotype.Component;
@Component
@Scope("singleton")
@Slf4j
public class UnsupportedParameterExceptionMapper implements ExceptionMapper<UnsupportedParameterException> {
public class UnsupportedParameterExceptionMapper implements FineractExceptionMapper, ExceptionMapper<UnsupportedParameterException> {
@Override
public Response toResponse(final UnsupportedParameterException exception) {
@@ -62,4 +62,9 @@ public class UnsupportedParameterExceptionMapper implements ExceptionMapper<Unsu
return Response.status(Status.BAD_REQUEST).entity(invalidParameterError).type(MediaType.APPLICATION_JSON).build();
}
@Override
public int errorCode() {
return 2001;
}
}
@@ -0,0 +1,137 @@
/**
* 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.batch.exception;
import static org.springframework.core.ResolvableType.forClassWithGenerics;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import java.util.InputMismatchException;
import java.util.Map;
import java.util.NoSuchElementException;
import org.apache.fineract.infrastructure.core.exceptionmapper.DefaultExceptionMapper;
import org.apache.fineract.infrastructure.core.exceptionmapper.FineractExceptionMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.ApplicationContext;
@ExtendWith(MockitoExtension.class)
class ErrorHandlerTest {
@Mock
DefaultExceptionMapper defaultExceptionMapper;
@Mock
ApplicationContext applicationContext;
@Mock(extraInterfaces = FineractExceptionMapper.class)
ExceptionMapper<NullPointerException> exceptionMapper;
@Mock(extraInterfaces = FineractExceptionMapper.class)
ExceptionMapper<InputMismatchException> inputMismatchMapper;
@Mock
Response response;
@InjectMocks
ErrorHandler errorHandler;
@Test
public void testErrorHandlerFound() {
// given
NullPointerException npe = new NullPointerException("Div by zero");
Mockito.when(applicationContext.getBeanNamesForType(forClassWithGenerics(ExceptionMapper.class, NullPointerException.class)))
.thenReturn(new String[] { "exceptionMapper1", "exceptionMapper2", "exceptionMapper3" });
Mockito.when(applicationContext.getBeanNamesForType(FineractExceptionMapper.class))
.thenReturn(new String[] { "exceptionMapper2", "exceptionMapper4" });
Mockito.when(applicationContext.getBean("exceptionMapper2")).thenReturn(exceptionMapper);
Mockito.when(exceptionMapper.toResponse(npe)).thenReturn(response);
Mockito.when(response.getStatus()).thenReturn(406);
Mockito.when(response.getEntity()).thenReturn(Map.of("Exception", "message body"));
Mockito.when(((FineractExceptionMapper) exceptionMapper).errorCode()).thenReturn(1234);
// when
ErrorInfo errorInfo = errorHandler.handle(npe);
// then
Assertions.assertEquals(1234, errorInfo.getErrorCode());
Assertions.assertEquals("{\n \"Exception\": \"message body\"\n}", errorInfo.getMessage());
Assertions.assertEquals(406, errorInfo.getStatusCode());
Mockito.verifyNoMoreInteractions(exceptionMapper);
Mockito.verifyNoInteractions(defaultExceptionMapper);
}
@Test
public void testErrorHandlerChecksParentException() {
// given
InputMismatchException ime = new InputMismatchException("Input Mismatch");
Mockito.when(applicationContext.getBeanNamesForType(forClassWithGenerics(ExceptionMapper.class, InputMismatchException.class)))
.thenReturn(new String[] {}); // no direct handler for InputMismatchException
Mockito.when(applicationContext.getBeanNamesForType(forClassWithGenerics(ExceptionMapper.class, NoSuchElementException.class)))
.thenReturn(new String[] { "inputMismatchMapper" });
Mockito.when(applicationContext.getBeanNamesForType(FineractExceptionMapper.class))
.thenReturn(new String[] { "inputMismatchMapper", "someOtherMapper", "yetAnotherMapper" });
Mockito.when(applicationContext.getBean("inputMismatchMapper")).thenReturn(inputMismatchMapper);
Mockito.when(inputMismatchMapper.toResponse(ime)).thenReturn(response);
Mockito.when(response.getStatus()).thenReturn(406);
Mockito.when(response.getEntity()).thenReturn(Map.of("Exception", "message body"));
Mockito.when(((FineractExceptionMapper) inputMismatchMapper).errorCode()).thenReturn(1234);
// when
ErrorInfo errorInfo = errorHandler.handle(ime);
// then
Assertions.assertEquals(1234, errorInfo.getErrorCode());
Assertions.assertEquals("{\n \"Exception\": \"message body\"\n}", errorInfo.getMessage());
Assertions.assertEquals(406, errorInfo.getStatusCode());
Mockito.verifyNoMoreInteractions(inputMismatchMapper);
Mockito.verifyNoInteractions(exceptionMapper);
Mockito.verifyNoInteractions(defaultExceptionMapper);
}
@Test
public void testErrorHandlerFallsBackToDefault() {
// given
NullPointerException npe = new NullPointerException("Div by zero");
Mockito.when(applicationContext.getBeanNamesForType(forClassWithGenerics(ExceptionMapper.class, NullPointerException.class)))
.thenReturn(new String[] {});
Mockito.when(applicationContext.getBeanNamesForType(FineractExceptionMapper.class))
.thenReturn(new String[] { "exceptionMapper2", "exceptionMapper4" });
Mockito.when(defaultExceptionMapper.toResponse(npe)).thenReturn(response);
Mockito.when(response.getStatus()).thenReturn(406);
Mockito.when(response.getEntity()).thenReturn(Map.of("Exception", "message body"));
Mockito.when(((FineractExceptionMapper) defaultExceptionMapper).errorCode()).thenReturn(1234);
// when
ErrorInfo errorInfo = errorHandler.handle(npe);
// then
Assertions.assertEquals(1234, errorInfo.getErrorCode());
Assertions.assertEquals("{\n \"Exception\": \"message body\"\n}", errorInfo.getMessage());
Assertions.assertEquals(406, errorInfo.getStatusCode());
Mockito.verifyNoInteractions(exceptionMapper);
Mockito.verifyNoMoreInteractions(defaultExceptionMapper);
}
}
@@ -0,0 +1,42 @@
/**
* 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.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class DefaultExceptionMapperTest {
@Test
public void testDefaultExceptionMapper() {
DefaultExceptionMapper defaultExceptionMapper = new DefaultExceptionMapper();
Response response = defaultExceptionMapper.toResponse(new RuntimeException("error happened"));
Assertions.assertEquals(9999, defaultExceptionMapper.errorCode());
Assertions.assertEquals(Map.of("Exception", "error happened"), response.getEntity());
Assertions.assertEquals(SC_INTERNAL_SERVER_ERROR, response.getStatus());
Assertions.assertEquals(MediaType.APPLICATION_JSON, response.getMediaType().toString());
}
}
@@ -0,0 +1,51 @@
/**
* 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.exception;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
import org.apache.fineract.infrastructure.core.exceptionmapper.FineractExceptionMapper;
import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("singleton")
@Slf4j
public class LinkedAccountRequiredExceptionMapper implements FineractExceptionMapper, ExceptionMapper<LinkedAccountRequiredException> {
@Override
public Response toResponse(LinkedAccountRequiredException exception) {
log.warn("Exception: {}, Message: {}", exception.getClass().getName(), exception.getMessage());
final ApiGlobalErrorResponse notFoundErrorResponse = ApiGlobalErrorResponse.domainRuleViolation(
exception.getGlobalisationMessageCode(), exception.getDefaultUserMessage(), exception.getDefaultUserMessageArgs());
// request understood but not carried out due to it violating some
// domain/business logic
return Response.status(Response.Status.FORBIDDEN).entity(notFoundErrorResponse).type(MediaType.APPLICATION_JSON).build();
}
@Override
public int errorCode() {
return 3002;
}
}
@@ -18,26 +18,31 @@
*/
package org.apache.fineract.infrastructure.core.exception;
import jakarta.annotation.PostConstruct;
import org.apache.fineract.batch.exception.ErrorHandler;
import org.apache.fineract.batch.exception.ErrorInfo;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
import org.apache.fineract.infrastructure.core.exceptionmapper.PlatformDomainRuleExceptionMapper;
import org.apache.fineract.infrastructure.core.exceptionmapper.FineractExceptionMapper;
import org.apache.fineract.infrastructure.jobs.exception.LoanIdsHardLockedException;
import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException;
import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
import org.apache.http.HttpStatus;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
public class ErrorHandlerRegister {
@Scope("singleton")
@Slf4j
public class LoanIdsHardLockedExceptionMapper implements FineractExceptionMapper, ExceptionMapper<LoanIdsHardLockedException> {
@PostConstruct
public void init() {
ErrorHandler.registerNewErrorHandler(LoanIdsHardLockedException.class, runtimeException -> new ErrorInfo(HttpStatus.SC_CONFLICT,
4090,
ApiGlobalErrorResponse.loanIsLocked(((LoanIdsHardLockedException) runtimeException).getLoanIdFromRequest()).toJson()));
ErrorHandler.registerNewErrorHandler(LinkedAccountRequiredException.class, new PlatformDomainRuleExceptionMapper(), 3002);
ErrorHandler.registerNewErrorHandler(MultiDisbursementDataRequiredException.class, new PlatformDomainRuleExceptionMapper(), 3003);
@Override
public Response toResponse(LoanIdsHardLockedException exception) {
return Response.status(HttpStatus.SC_CONFLICT)
.entity(ApiGlobalErrorResponse.loanIsLocked(exception.getLoanIdFromRequest()).toJson()).type(MediaType.APPLICATION_JSON)
.build();
}
@Override
public int errorCode() {
return 4090;
}
}
@@ -0,0 +1,51 @@
/**
* 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.exception;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
import org.apache.fineract.infrastructure.core.exceptionmapper.FineractExceptionMapper;
import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("singleton")
@Slf4j
public class MultiDisbursementDataRequiredExceptionMapper
implements FineractExceptionMapper, ExceptionMapper<MultiDisbursementDataRequiredException> {
@Override
public Response toResponse(MultiDisbursementDataRequiredException exception) {
log.warn("Exception: {}, Message: {}", exception.getClass().getName(), exception.getMessage());
final ApiGlobalErrorResponse notFoundErrorResponse = ApiGlobalErrorResponse.domainRuleViolation(
exception.getGlobalisationMessageCode(), exception.getDefaultUserMessage(), exception.getDefaultUserMessageArgs());
// request understood but not carried out due to it violating some
// domain/business logic
return Response.status(Response.Status.FORBIDDEN).entity(notFoundErrorResponse).type(MediaType.APPLICATION_JSON).build();
}
@Override
public int errorCode() {
return 3003;
}
}
@@ -0,0 +1,46 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.core.exception;
import static org.apache.http.HttpStatus.SC_FORBIDDEN;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class LinkedAccountRequiredExceptionMapperTest {
@Test
public void testExceptionMapper() {
LinkedAccountRequiredExceptionMapper exceptionMapper = new LinkedAccountRequiredExceptionMapper();
LinkedAccountRequiredException exception = new LinkedAccountRequiredException("entity", "message", "args");
Response response = exceptionMapper.toResponse(exception);
Assertions.assertEquals(3002, exceptionMapper.errorCode());
Assertions.assertInstanceOf(ApiGlobalErrorResponse.class, response.getEntity());
Assertions.assertEquals(1, ((ApiGlobalErrorResponse) response.getEntity()).getErrors().size());
Assertions.assertEquals("message", ((ApiGlobalErrorResponse) response.getEntity()).getErrors().get(0).getDefaultUserMessage());
Assertions.assertEquals(SC_FORBIDDEN, response.getStatus());
Assertions.assertEquals(MediaType.APPLICATION_JSON, response.getMediaType().toString());
}
}
@@ -0,0 +1,41 @@
/**
* 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.exception;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.apache.fineract.infrastructure.jobs.exception.LoanIdsHardLockedException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class LoanIdsHardLockedExceptionMapperTest {
@Test
public void testExceptionMapper() {
LoanIdsHardLockedExceptionMapper exceptionMapper = new LoanIdsHardLockedExceptionMapper();
Response response = exceptionMapper.toResponse(new LoanIdsHardLockedException(123L));
Assertions.assertEquals(4090, exceptionMapper.errorCode());
Assertions.assertEquals("{\"developerMessage\":\"Loan is locked by the COB job. Loan ID: 123\","
+ "\"httpStatusCode\":\"Conflict\"," + "\"defaultUserMessage\":\"Loan is locked by the COB job. Loan ID: \\\" + loanId\","
+ "\"userMessageGlobalisationCode\":\"error.msg.loan.locked\"," + "\"errors\":[]}", response.getEntity());
Assertions.assertEquals(409, response.getStatus());
Assertions.assertEquals(MediaType.APPLICATION_JSON, response.getMediaType().toString());
}
}
@@ -0,0 +1,46 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.core.exception;
import static org.apache.http.HttpStatus.SC_FORBIDDEN;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class MultiDisbursementDataRequiredExceptionMapperTest {
@Test
public void testExceptionMapper() {
MultiDisbursementDataRequiredExceptionMapper exceptionMapper = new MultiDisbursementDataRequiredExceptionMapper();
MultiDisbursementDataRequiredException exception = new MultiDisbursementDataRequiredException("entity", "message", "args");
Response response = exceptionMapper.toResponse(exception);
Assertions.assertEquals(3003, exceptionMapper.errorCode());
Assertions.assertInstanceOf(ApiGlobalErrorResponse.class, response.getEntity());
Assertions.assertEquals(1, ((ApiGlobalErrorResponse) response.getEntity()).getErrors().size());
Assertions.assertEquals("message", ((ApiGlobalErrorResponse) response.getEntity()).getErrors().get(0).getDefaultUserMessage());
Assertions.assertEquals(SC_FORBIDDEN, response.getStatus());
Assertions.assertEquals(MediaType.APPLICATION_JSON, response.getMediaType().toString());
}
}
@@ -19,9 +19,11 @@
package org.apache.fineract.commands.service;
import static org.apache.fineract.commands.domain.CommandProcessingResultType.UNDER_PROCESSING;
import static org.mockito.ArgumentMatchers.any;
import java.time.ZoneId;
import java.util.Optional;
import org.apache.fineract.batch.exception.ErrorHandler;
import org.apache.fineract.batch.exception.ErrorInfo;
import org.apache.fineract.commands.domain.CommandSource;
import org.apache.fineract.commands.domain.CommandSourceRepository;
@@ -46,6 +48,9 @@ public class CommandSourceServiceTest {
@Mock
private CommandSourceRepository commandSourceRepository;
@Mock
private ErrorHandler errorHandler;
@InjectMocks
private CommandSourceService underTest;
@@ -103,6 +108,8 @@ public class CommandSourceServiceTest {
@Test
public void testGenerateErrorException() {
Mockito.when(errorHandler.handle(any(CodeNotFoundException.class)))
.thenReturn(new ErrorInfo(404, 1001, "Code with name `foo` does not exist"));
ErrorInfo result = underTest.generateErrorException(new CodeNotFoundException("foo"));
Assertions.assertEquals(404, result.getStatusCode());
Assertions.assertEquals(1001, result.getErrorCode());