diff --git a/fineract-core/src/main/java/org/apache/fineract/batch/exception/ErrorHandler.java b/fineract-core/src/main/java/org/apache/fineract/batch/exception/ErrorHandler.java index ed172df98..babc57c05 100644 --- a/fineract-core/src/main/java/org/apache/fineract/batch/exception/ErrorHandler.java +++ b/fineract-core/src/main/java/org/apache/fineract/batch/exception/ErrorHandler.java @@ -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, Function> EXCEPTION_HANDLERS = Collections - .synchronizedMap(new LinkedHashMap<>()); - private static final Map.Entry, Function> 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 ExceptionMapper findMostSpecificExceptionHandler(T exception) { + Class clazz = exception.getClass(); + do { + Set exceptionMappers = createSet(ctx.getBeanNamesForType(forClassWithGenerics(ExceptionMapper.class, clazz))); + Set fineractErrorMappers = createSet(ctx.getBeanNamesForType(FineractExceptionMapper.class)); + SetUtils.SetView intersection = SetUtils.intersection(exceptionMappers, fineractErrorMappers); + if (intersection.size() > 0) { + // noinspection unchecked + return (ExceptionMapper) ctx.getBean(intersection.iterator().next()); + } + clazz = clazz.getSuperclass(); + } while (!clazz.equals(Exception.class)); + // noinspection unchecked + return (ExceptionMapper) defaultExceptionMapper; } - private ErrorHandler() {} - - public static void registerNewErrorHandler(final Class exceptionClass, final ExceptionMapper mapper, - int errorCode) { - LinkedHashMap, Function> 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 exceptionClass, - Function function) { - LinkedHashMap, Function> 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 Set 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, Function> 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 exceptionMapper = findMostSpecificExceptionHandler(exception); + Response response = exceptionMapper.toResponse(exception); + return new ErrorInfo(response.getStatus(), ((FineractExceptionMapper) exceptionMapper).errorCode(), + JSON_HELPER.toJson(response.getEntity())); } } diff --git a/fineract-core/src/main/java/org/apache/fineract/batch/service/BatchApiServiceImpl.java b/fineract-core/src/main/java/org/apache/fineract/batch/service/BatchApiServiceImpl.java index 71f06ac67..dfb43a856 100644 --- a/fineract-core/src/main/java/org/apache/fineract/batch/service/BatchApiServiceImpl.java +++ b/fineract-core/src/main/java/org/apache/fineract/batch/service/BatchApiServiceImpl.java @@ -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 batchFilters; @@ -106,7 +107,7 @@ public class BatchApiServiceImpl implements BatchApiService { final List 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 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()); } } diff --git a/fineract-core/src/main/java/org/apache/fineract/batch/service/BatchExecutionException.java b/fineract-core/src/main/java/org/apache/fineract/batch/service/BatchExecutionException.java index 4b9ad606a..d0877a14b 100644 --- a/fineract-core/src/main/java/org/apache/fineract/batch/service/BatchExecutionException.java +++ b/fineract-core/src/main/java/org/apache/fineract/batch/service/BatchExecutionException.java @@ -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; } } diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java index 39420ec1a..f58c7ef8d 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java @@ -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() + "}"); } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/DefaultExceptionMapper.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/DefaultExceptionMapper.java new file mode 100644 index 000000000..5d3e48b24 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/DefaultExceptionMapper.java @@ -0,0 +1,46 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.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 { + + @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(); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/FineractExceptionMapper.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/FineractExceptionMapper.java new file mode 100644 index 000000000..df6f8cc5d --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/FineractExceptionMapper.java @@ -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(); +} diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformApiDataValidationExceptionMapper.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformApiDataValidationExceptionMapper.java index a01d0f50c..728317329 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformApiDataValidationExceptionMapper.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformApiDataValidationExceptionMapper.java @@ -40,7 +40,8 @@ import org.springframework.stereotype.Component; @Component @Scope("singleton") @Slf4j -public class PlatformApiDataValidationExceptionMapper implements ExceptionMapper { +public class PlatformApiDataValidationExceptionMapper + implements FineractExceptionMapper, ExceptionMapper { @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; + } } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformDataIntegrityExceptionMapper.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformDataIntegrityExceptionMapper.java index 991e092a5..ba131591f 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformDataIntegrityExceptionMapper.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformDataIntegrityExceptionMapper.java @@ -40,7 +40,7 @@ import org.springframework.stereotype.Component; @Component @Scope("singleton") @Slf4j -public class PlatformDataIntegrityExceptionMapper implements ExceptionMapper { +public class PlatformDataIntegrityExceptionMapper implements FineractExceptionMapper, ExceptionMapper { @Override public Response toResponse(final PlatformDataIntegrityException exception) { @@ -50,4 +50,9 @@ public class PlatformDataIntegrityExceptionMapper implements ExceptionMapper { +public class PlatformDomainRuleExceptionMapper implements FineractExceptionMapper, ExceptionMapper { @Override public Response toResponse(final AbstractPlatformDomainRuleException exception) { @@ -51,4 +51,9 @@ public class PlatformDomainRuleExceptionMapper implements ExceptionMapper { +public class PlatformInternalServerExceptionMapper implements FineractExceptionMapper, ExceptionMapper { @Override public Response toResponse(final PlatformInternalServerException exception) { @@ -48,4 +48,9 @@ public class PlatformInternalServerExceptionMapper implements ExceptionMapper { +public class PlatformResourceNotFoundExceptionMapper + implements FineractExceptionMapper, ExceptionMapper { @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; + } } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/UnsupportedParameterExceptionMapper.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/UnsupportedParameterExceptionMapper.java index 926d9bc66..8abda8b0d 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/UnsupportedParameterExceptionMapper.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/UnsupportedParameterExceptionMapper.java @@ -40,7 +40,7 @@ import org.springframework.stereotype.Component; @Component @Scope("singleton") @Slf4j -public class UnsupportedParameterExceptionMapper implements ExceptionMapper { +public class UnsupportedParameterExceptionMapper implements FineractExceptionMapper, ExceptionMapper { @Override public Response toResponse(final UnsupportedParameterException exception) { @@ -62,4 +62,9 @@ public class UnsupportedParameterExceptionMapper implements ExceptionMapper exceptionMapper; + + @Mock(extraInterfaces = FineractExceptionMapper.class) + ExceptionMapper 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); + } + +} diff --git a/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/exceptionmapper/DefaultExceptionMapperTest.java b/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/exceptionmapper/DefaultExceptionMapperTest.java new file mode 100644 index 000000000..54afb5acb --- /dev/null +++ b/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/exceptionmapper/DefaultExceptionMapperTest.java @@ -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()); + } + +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/LinkedAccountRequiredExceptionMapper.java b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/LinkedAccountRequiredExceptionMapper.java new file mode 100644 index 000000000..9e69d82ea --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/LinkedAccountRequiredExceptionMapper.java @@ -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 { + + @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; + } + +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/ErrorHandlerRegister.java b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/LoanIdsHardLockedExceptionMapper.java similarity index 50% rename from fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/ErrorHandlerRegister.java rename to fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/LoanIdsHardLockedExceptionMapper.java index 474da1994..7f47c4056 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/ErrorHandlerRegister.java +++ b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/LoanIdsHardLockedExceptionMapper.java @@ -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 { - @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; } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/MultiDisbursementDataRequiredExceptionMapper.java b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/MultiDisbursementDataRequiredExceptionMapper.java new file mode 100644 index 000000000..931cee3a8 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/MultiDisbursementDataRequiredExceptionMapper.java @@ -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 { + + @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; + } +} diff --git a/fineract-loan/src/test/java/org/apache/fineract/infrastructure/core/exception/LinkedAccountRequiredExceptionMapperTest.java b/fineract-loan/src/test/java/org/apache/fineract/infrastructure/core/exception/LinkedAccountRequiredExceptionMapperTest.java new file mode 100644 index 000000000..1cc2f4736 --- /dev/null +++ b/fineract-loan/src/test/java/org/apache/fineract/infrastructure/core/exception/LinkedAccountRequiredExceptionMapperTest.java @@ -0,0 +1,46 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.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()); + } + +} diff --git a/fineract-loan/src/test/java/org/apache/fineract/infrastructure/core/exception/LoanIdsHardLockedExceptionMapperTest.java b/fineract-loan/src/test/java/org/apache/fineract/infrastructure/core/exception/LoanIdsHardLockedExceptionMapperTest.java new file mode 100644 index 000000000..bc838faee --- /dev/null +++ b/fineract-loan/src/test/java/org/apache/fineract/infrastructure/core/exception/LoanIdsHardLockedExceptionMapperTest.java @@ -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()); + } +} diff --git a/fineract-loan/src/test/java/org/apache/fineract/infrastructure/core/exception/MultiDisbursementDataRequiredExceptionMapperTest.java b/fineract-loan/src/test/java/org/apache/fineract/infrastructure/core/exception/MultiDisbursementDataRequiredExceptionMapperTest.java new file mode 100644 index 000000000..75d15c2d2 --- /dev/null +++ b/fineract-loan/src/test/java/org/apache/fineract/infrastructure/core/exception/MultiDisbursementDataRequiredExceptionMapperTest.java @@ -0,0 +1,46 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.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()); + } + +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandSourceServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandSourceServiceTest.java index b28cc7de7..96fa7b0de 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandSourceServiceTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandSourceServiceTest.java @@ -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());