diff --git a/pom.xml b/pom.xml index 0a8f96e..57e456a 100644 --- a/pom.xml +++ b/pom.xml @@ -50,6 +50,9 @@ gridsuite org.gridsuite:computation + + + 1.34.0-SNAPSHOT @@ -173,6 +176,7 @@ com.powsybl powsybl-ws-commons + ${powsybl-ws-commons.version} true diff --git a/src/main/java/org/gridsuite/computation/ComputationException.java b/src/main/java/org/gridsuite/computation/ComputationException.java deleted file mode 100644 index c19157f..0000000 --- a/src/main/java/org/gridsuite/computation/ComputationException.java +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright (c) 2025, RTE (http://www.rte-france.com) - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.gridsuite.computation; - -import lombok.Getter; - -import java.util.Objects; - -/** - * @author Anis Touri - */ -@Getter -public class ComputationException extends RuntimeException { - public enum Type { - RESULT_NOT_FOUND("Result not found."), - INVALID_FILTER_FORMAT("The filter format is invalid."), - INVALID_SORT_FORMAT("The sort format is invalid"), - INVALID_FILTER("Invalid filter"), - NETWORK_NOT_FOUND("Network not found"), - PARAMETERS_NOT_FOUND("Computation parameters not found"), - FILE_EXPORT_ERROR("Error exporting the file"), - EVALUATE_FILTER_FAILED("Error evaluating the file"), - LIMIT_REDUCTION_CONFIG_ERROR("Error int the configuration of the limit reduction"), - SPECIFIC("Unknown error during the computation"); - - private final String defaultMessage; - - Type(String defaultMessage) { - this.defaultMessage = defaultMessage; - } - } - - private final Type exceptionType; - - public ComputationException(Type exceptionType) { - super(Objects.requireNonNull(exceptionType.defaultMessage)); - this.exceptionType = Objects.requireNonNull(exceptionType); - } - - public ComputationException(String message) { - super(message); - this.exceptionType = Type.SPECIFIC; - } - - public ComputationException(String message, Throwable cause) { - super(message, cause); - this.exceptionType = Type.SPECIFIC; - } - - public ComputationException(Type exceptionType, String message) { - super(message); - this.exceptionType = Objects.requireNonNull(exceptionType); - } - - public ComputationException(Type exceptionType, String message, Throwable cause) { - super(message, cause); - this.exceptionType = Objects.requireNonNull(exceptionType); - } -} diff --git a/src/main/java/org/gridsuite/computation/error/ComputationBusinessErrorCode.java b/src/main/java/org/gridsuite/computation/error/ComputationBusinessErrorCode.java new file mode 100644 index 0000000..8345778 --- /dev/null +++ b/src/main/java/org/gridsuite/computation/error/ComputationBusinessErrorCode.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.computation.error; + +import com.powsybl.ws.commons.error.BusinessErrorCode; + +public enum ComputationBusinessErrorCode implements BusinessErrorCode { + RESULT_NOT_FOUND("computation.resultNotFound"), + PARAMETERS_NOT_FOUND("computation.parametersNotFound"), + INVALID_SORT_FORMAT("computation.invalidSortFormat"), + INVALID_EXPORT_PARAMS("computation.invalidExportParams"), + LIMIT_REDUCTION_CONFIG_ERROR("computation.limitReductionConfigError"), + RUNNER_ERROR("computation.runnerError"); + + private final String code; + + ComputationBusinessErrorCode(String code) { + this.code = code; + } + + public String value() { + return code; + } +} diff --git a/src/main/java/org/gridsuite/computation/error/ComputationException.java b/src/main/java/org/gridsuite/computation/error/ComputationException.java new file mode 100644 index 0000000..71e54ef --- /dev/null +++ b/src/main/java/org/gridsuite/computation/error/ComputationException.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.computation.error; + +import com.powsybl.ws.commons.error.AbstractBusinessException; +import lombok.Getter; +import lombok.NonNull; + +import java.util.Objects; + +/** + * @author Anis Touri + */ +@Getter +public class ComputationException extends AbstractBusinessException { + + private final ComputationBusinessErrorCode errorCode; + + @NonNull + @Override + public ComputationBusinessErrorCode getBusinessErrorCode() { + return errorCode; + } + + public ComputationException(ComputationBusinessErrorCode errorCode, String message, Throwable cause) { + super(message, cause); + this.errorCode = errorCode; + } + + public ComputationException(ComputationBusinessErrorCode errorCode, String message) { + super(message); + this.errorCode = Objects.requireNonNull(errorCode); + } +} diff --git a/src/main/java/org/gridsuite/computation/error/ComputationRestResponseEntityExceptionHandler.java b/src/main/java/org/gridsuite/computation/error/ComputationRestResponseEntityExceptionHandler.java new file mode 100644 index 0000000..9eab305 --- /dev/null +++ b/src/main/java/org/gridsuite/computation/error/ComputationRestResponseEntityExceptionHandler.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.computation.error; + +import com.powsybl.ws.commons.error.AbstractBusinessExceptionHandler; +import com.powsybl.ws.commons.error.ServerNameProvider; +import lombok.NonNull; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ControllerAdvice; + +/** + * @author Hugo Marcellin + */ + +@ControllerAdvice +public class ComputationRestResponseEntityExceptionHandler extends AbstractBusinessExceptionHandler { + + protected ComputationRestResponseEntityExceptionHandler(ServerNameProvider serverNameProvider) { + super(serverNameProvider); + } + + @Override + protected @NonNull ComputationBusinessErrorCode getBusinessCode(ComputationException e) { + return e.getBusinessErrorCode(); + } + + @Override + protected HttpStatus mapStatus(ComputationBusinessErrorCode businessErrorCode) { + return switch (businessErrorCode) { + case RESULT_NOT_FOUND, PARAMETERS_NOT_FOUND -> HttpStatus.NOT_FOUND; + case INVALID_SORT_FORMAT, INVALID_EXPORT_PARAMS -> HttpStatus.BAD_REQUEST; + default -> HttpStatus.INTERNAL_SERVER_ERROR; + }; + } +} diff --git a/src/main/java/org/gridsuite/computation/service/AbstractWorkerService.java b/src/main/java/org/gridsuite/computation/service/AbstractWorkerService.java index 77d9140..2dfdddb 100644 --- a/src/main/java/org/gridsuite/computation/service/AbstractWorkerService.java +++ b/src/main/java/org/gridsuite/computation/service/AbstractWorkerService.java @@ -16,7 +16,8 @@ import com.powsybl.network.store.client.PreloadingStrategy; import com.powsybl.ws.commons.ZipUtils; import org.apache.commons.lang3.StringUtils; -import org.gridsuite.computation.ComputationException; +import org.gridsuite.computation.error.ComputationBusinessErrorCode; +import org.gridsuite.computation.error.ComputationException; import org.gridsuite.computation.s3.ComputationS3Service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -180,7 +181,7 @@ public Consumer> consumeRun() { } catch (Exception e) { resultService.delete(resultContext.getResultUuid()); this.handleNonCancellationException(resultContext, e, rootReporter); - throw new ComputationException(String.format("%s: %s", NotificationService.getFailedMessage(getComputationType()), e.getMessage()), e.getCause()); + throw new ComputationException(ComputationBusinessErrorCode.RUNNER_ERROR, String.format("%s: %s", NotificationService.getFailedMessage(getComputationType()), e.getMessage()), e.getCause()); } finally { if (Boolean.TRUE.equals(resultContext.getRunContext().getDebug())) { processDebug(resultContext); diff --git a/src/main/java/org/gridsuite/computation/utils/FilterUtils.java b/src/main/java/org/gridsuite/computation/utils/FilterUtils.java index 04416f3..6cead98 100644 --- a/src/main/java/org/gridsuite/computation/utils/FilterUtils.java +++ b/src/main/java/org/gridsuite/computation/utils/FilterUtils.java @@ -10,10 +10,10 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.StringUtils; -import org.gridsuite.computation.ComputationException; import org.gridsuite.computation.dto.GlobalFilter; import org.gridsuite.computation.dto.ResourceFilterDTO; +import java.io.UncheckedIOException; import java.util.List; /** @@ -33,7 +33,7 @@ private static T fromStringToDTO(String jsonString, ObjectMapper objectMappe try { return objectMapper.readValue(jsonString, typeReference); } catch (JsonProcessingException e) { - throw new ComputationException(ComputationException.Type.INVALID_FILTER_FORMAT); + throw new UncheckedIOException(e); } } diff --git a/src/test/java/org/gridsuite/computation/ComputationBusinessErrorCodeTest.java b/src/test/java/org/gridsuite/computation/ComputationBusinessErrorCodeTest.java new file mode 100644 index 0000000..796f73c --- /dev/null +++ b/src/test/java/org/gridsuite/computation/ComputationBusinessErrorCodeTest.java @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.computation; + +import org.gridsuite.computation.error.ComputationBusinessErrorCode; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class ComputationBusinessErrorCodeTest { + @ParameterizedTest + @EnumSource(ComputationBusinessErrorCode.class) + void valueMatchesEnumName(ComputationBusinessErrorCode code) { + assertThat(code.value()).startsWith("computation."); + } +} diff --git a/src/test/java/org/gridsuite/computation/ComputationExceptionTest.java b/src/test/java/org/gridsuite/computation/ComputationExceptionTest.java index fb9b670..75a2c37 100644 --- a/src/test/java/org/gridsuite/computation/ComputationExceptionTest.java +++ b/src/test/java/org/gridsuite/computation/ComputationExceptionTest.java @@ -6,8 +6,11 @@ */ package org.gridsuite.computation; +import org.gridsuite.computation.error.ComputationException; import org.junit.jupiter.api.Test; +import static org.gridsuite.computation.error.ComputationBusinessErrorCode.PARAMETERS_NOT_FOUND; +import static org.gridsuite.computation.error.ComputationBusinessErrorCode.RUNNER_ERROR; import static org.junit.jupiter.api.Assertions.*; /** @@ -16,16 +19,18 @@ class ComputationExceptionTest { @Test - void testMessageConstructor() { - var e = new ComputationException("test"); + void testMessageAndThrowableConstructor() { + var cause = new RuntimeException("test"); + var e = new ComputationException(RUNNER_ERROR, "test", cause); + assertEquals(RUNNER_ERROR, e.getBusinessErrorCode()); assertEquals("test", e.getMessage()); + assertEquals(cause, e.getCause()); } @Test - void testMessageAndThrowableConstructor() { - var cause = new RuntimeException("test"); - var e = new ComputationException("test", cause); + void testBusinessErrorCodeConstructor() { + var e = new ComputationException(PARAMETERS_NOT_FOUND, "test"); assertEquals("test", e.getMessage()); - assertEquals(cause, e.getCause()); + assertEquals(PARAMETERS_NOT_FOUND, e.getBusinessErrorCode()); } } diff --git a/src/test/java/org/gridsuite/computation/ComputationTest.java b/src/test/java/org/gridsuite/computation/ComputationTest.java index d3b110f..4719575 100644 --- a/src/test/java/org/gridsuite/computation/ComputationTest.java +++ b/src/test/java/org/gridsuite/computation/ComputationTest.java @@ -26,6 +26,7 @@ import lombok.extern.slf4j.Slf4j; import org.assertj.core.api.WithAssertions; import org.gridsuite.computation.dto.ReportInfos; +import org.gridsuite.computation.error.ComputationException; import org.gridsuite.computation.s3.ComputationS3Service; import org.gridsuite.computation.s3.S3InputStreamInfos; import org.gridsuite.computation.service.*; diff --git a/src/test/java/org/gridsuite/computation/error/RestResponseEntityExceptionHandlerTest.java b/src/test/java/org/gridsuite/computation/error/RestResponseEntityExceptionHandlerTest.java new file mode 100644 index 0000000..f4c8ac7 --- /dev/null +++ b/src/test/java/org/gridsuite/computation/error/RestResponseEntityExceptionHandlerTest.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.computation.error; + +import com.powsybl.ws.commons.error.PowsyblWsProblemDetail; +import org.gridsuite.computation.error.utils.TestRestResponseEntityExceptionHandler; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockHttpServletRequest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.gridsuite.computation.error.ComputationBusinessErrorCode.INVALID_SORT_FORMAT; +import static org.gridsuite.computation.error.ComputationBusinessErrorCode.RESULT_NOT_FOUND; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class RestResponseEntityExceptionHandlerTest { + + private TestRestResponseEntityExceptionHandler handler; + + @BeforeEach + void setUp() { + handler = new TestRestResponseEntityExceptionHandler(); + } + + @Test + void mapsNotFoundBusinessErrorToStatus() { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/results-endpoint/uuid"); + ComputationException exception = new ComputationException(RESULT_NOT_FOUND, "Result not found"); + ResponseEntity response = handler.invokeHandleDomainException(exception, request); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + assertThat(response.getBody()).isNotNull(); + assertEquals("computation.resultNotFound", response.getBody().getBusinessErrorCode()); + } + + @Test + void mapsBadRequestBusinessErrorToStatus() { + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/results-endpoint/uuid"); + ComputationException exception = new ComputationException(INVALID_SORT_FORMAT, "Invalid sort format"); + ResponseEntity response = handler.invokeHandleDomainException(exception, request); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(response.getBody()).isNotNull(); + assertEquals("computation.invalidSortFormat", response.getBody().getBusinessErrorCode()); + } +} diff --git a/src/test/java/org/gridsuite/computation/error/utils/TestRestResponseEntityExceptionHandler.java b/src/test/java/org/gridsuite/computation/error/utils/TestRestResponseEntityExceptionHandler.java new file mode 100644 index 0000000..6ca094d --- /dev/null +++ b/src/test/java/org/gridsuite/computation/error/utils/TestRestResponseEntityExceptionHandler.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.computation.error.utils; + +import com.powsybl.ws.commons.error.PowsyblWsProblemDetail; +import org.gridsuite.computation.error.ComputationException; +import org.gridsuite.computation.error.ComputationRestResponseEntityExceptionHandler; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockHttpServletRequest; + +/** + * @author Hugo Marcellin + */ +public class TestRestResponseEntityExceptionHandler extends ComputationRestResponseEntityExceptionHandler { + public TestRestResponseEntityExceptionHandler() { + super(() -> "computation-server"); + } + + public ResponseEntity invokeHandleDomainException(ComputationException exception, MockHttpServletRequest request) { + return super.handleDomainException(exception, request); + } +} diff --git a/src/test/java/org/gridsuite/computation/utils/FilterUtilsTest.java b/src/test/java/org/gridsuite/computation/utils/FilterUtilsTest.java index a392c7a..12a0e70 100644 --- a/src/test/java/org/gridsuite/computation/utils/FilterUtilsTest.java +++ b/src/test/java/org/gridsuite/computation/utils/FilterUtilsTest.java @@ -10,20 +10,16 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.powsybl.iidm.network.Country; import com.powsybl.security.LimitViolationType; -import org.gridsuite.computation.ComputationException; import org.gridsuite.computation.dto.GlobalFilter; import org.gridsuite.computation.dto.ResourceFilterDTO; import org.junit.jupiter.api.Test; +import java.io.UncheckedIOException; import java.util.List; import java.util.Map; import java.util.UUID; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; /** * @author Franck Lecuyer @@ -73,7 +69,7 @@ void testFromStringGlobalFiltersToDTO() throws Exception { @Test void testInvalidFilterFormat() { - assertThrows(ComputationException.class, () -> FilterUtils.fromStringGlobalFiltersToDTO("titi", objectMapper), "The filter format is invalid."); + assertThrows(UncheckedIOException.class, () -> FilterUtils.fromStringGlobalFiltersToDTO("titi", objectMapper), "The filter format is invalid."); } @Test