From 4a1c7dc5eb92ee37586be59819a0ae125cff872b Mon Sep 17 00:00:00 2001 From: Rehili Ghazwa Date: Tue, 15 Jul 2025 16:49:21 +0200 Subject: [PATCH 01/10] First implementation for gridsuite-computation lib --- .github/workflows/build.yml | 16 + .github/workflows/patch.yml | 18 + .github/workflows/release.yml | 21 + .gitignore | 12 + .mvn/lombok-config-copy.marker | 0 README.md | 8 +- lombok.config | 1 + pom.xml | 312 +++++ .../computation/ComputationConfig.java | 23 + .../computation/ComputationException.java | 63 + .../computation/dto/GlobalFilter.java | 37 + .../computation/dto/ReportInfos.java | 24 + .../computation/dto/ResourceFilterDTO.java | 51 + .../service/AbstractComputationObserver.java | 107 ++ .../AbstractComputationResultService.java | 25 + .../AbstractComputationRunContext.java | 46 + .../service/AbstractComputationService.java | 79 ++ .../service/AbstractFilterService.java | 400 ++++++ .../service/AbstractResultContext.java | 77 ++ .../service/AbstractWorkerService.java | 260 ++++ .../computation/service/CancelContext.java | 50 + .../computation/service/ExecutionService.java | 44 + .../service/NotificationService.java | 119 ++ .../computation/service/ReportService.java | 82 ++ .../service/UuidGeneratorService.java | 22 + .../AbstractCommonSpecificationBuilder.java | 85 ++ .../utils/ComputationResultUtils.java | 96 ++ .../computation/utils/FilterUtils.java | 49 + .../computation/utils/MessageUtils.java | 39 + .../computation/utils/SpecificationUtils.java | 267 ++++ .../utils/annotations/PostCompletion.java | 21 + .../annotations/PostCompletionAdapter.java | 45 + .../PostCompletionAnnotationAspect.java | 36 + .../annotations/PostCompletionException.java | 16 + .../computation/ComputationExceptionTest.java | 31 + .../computation/ComputationTest.java | 330 +++++ .../computation/ComputationUtilTest.java | 179 +++ .../service/FilterServiceTest.java | 1146 +++++++++++++++++ .../service/ReportServiceTest.java | 95 ++ .../CommonSpecificationBuilderTest.java | 164 +++ src/test/resources/i18n/reports.properties | 1 + 41 files changed, 4496 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/patch.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 .mvn/lombok-config-copy.marker create mode 100644 lombok.config create mode 100644 pom.xml create mode 100644 src/main/java/org/gridsuite/computation/ComputationConfig.java create mode 100644 src/main/java/org/gridsuite/computation/ComputationException.java create mode 100644 src/main/java/org/gridsuite/computation/dto/GlobalFilter.java create mode 100644 src/main/java/org/gridsuite/computation/dto/ReportInfos.java create mode 100644 src/main/java/org/gridsuite/computation/dto/ResourceFilterDTO.java create mode 100644 src/main/java/org/gridsuite/computation/service/AbstractComputationObserver.java create mode 100644 src/main/java/org/gridsuite/computation/service/AbstractComputationResultService.java create mode 100644 src/main/java/org/gridsuite/computation/service/AbstractComputationRunContext.java create mode 100644 src/main/java/org/gridsuite/computation/service/AbstractComputationService.java create mode 100644 src/main/java/org/gridsuite/computation/service/AbstractFilterService.java create mode 100644 src/main/java/org/gridsuite/computation/service/AbstractResultContext.java create mode 100644 src/main/java/org/gridsuite/computation/service/AbstractWorkerService.java create mode 100644 src/main/java/org/gridsuite/computation/service/CancelContext.java create mode 100644 src/main/java/org/gridsuite/computation/service/ExecutionService.java create mode 100644 src/main/java/org/gridsuite/computation/service/NotificationService.java create mode 100644 src/main/java/org/gridsuite/computation/service/ReportService.java create mode 100644 src/main/java/org/gridsuite/computation/service/UuidGeneratorService.java create mode 100644 src/main/java/org/gridsuite/computation/specification/AbstractCommonSpecificationBuilder.java create mode 100644 src/main/java/org/gridsuite/computation/utils/ComputationResultUtils.java create mode 100644 src/main/java/org/gridsuite/computation/utils/FilterUtils.java create mode 100644 src/main/java/org/gridsuite/computation/utils/MessageUtils.java create mode 100644 src/main/java/org/gridsuite/computation/utils/SpecificationUtils.java create mode 100644 src/main/java/org/gridsuite/computation/utils/annotations/PostCompletion.java create mode 100644 src/main/java/org/gridsuite/computation/utils/annotations/PostCompletionAdapter.java create mode 100644 src/main/java/org/gridsuite/computation/utils/annotations/PostCompletionAnnotationAspect.java create mode 100644 src/main/java/org/gridsuite/computation/utils/annotations/PostCompletionException.java create mode 100644 src/test/java/org/gridsuite/computation/ComputationExceptionTest.java create mode 100644 src/test/java/org/gridsuite/computation/ComputationTest.java create mode 100644 src/test/java/org/gridsuite/computation/ComputationUtilTest.java create mode 100644 src/test/java/org/gridsuite/computation/service/FilterServiceTest.java create mode 100644 src/test/java/org/gridsuite/computation/service/ReportServiceTest.java create mode 100644 src/test/java/org/gridsuite/computation/specification/CommonSpecificationBuilderTest.java create mode 100644 src/test/resources/i18n/reports.properties diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..67169fb --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,16 @@ +name: CI + +on: + push: + branches: + - 'main' + pull_request: + +jobs: + build: + uses: powsybl/github-ci/.github/workflows/build-backend-lib-generic.yml@39565de6fd7d394ed76fa09e5197ffb1350ff1e6 + with: + eventType: computation_updated + secrets: + sonar-token: ${{ secrets.SONAR_TOKEN }} + repo-token: ${{ secrets.REPO_ACCESS_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml new file mode 100644 index 0000000..0a51447 --- /dev/null +++ b/.github/workflows/patch.yml @@ -0,0 +1,18 @@ +name: Patch + +on: + workflow_dispatch: + inputs: + branchRef: + description: 'Patch branch (format: release-vX.Y.Z)' + required: true + type: string + +jobs: + run-patch: + uses: powsybl/github-ci/.github/workflows/patch-backend-lib-generic.yml@39565de6fd7d394ed76fa09e5197ffb1350ff1e6 + with: + githubappId: ${{ vars.GRIDSUITE_ACTIONS_APPID }} + branchRef: ${{ github.event.inputs.branchRef }} + secrets: + VERSIONBUMP_GHAPP_PRIVATE_KEY: ${{ secrets.VERSIONBUMP_GHAPP_PRIVATE_KEY }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4c67f37 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,21 @@ +name: Release + +on: + workflow_dispatch: + inputs: + versionType: + description: 'Version type increment (major | minor)' + required: true + type: choice + options: + - major + - minor + +jobs: + run-release: + uses: powsybl/github-ci/.github/workflows/release-backend-lib-generic.yml@39565de6fd7d394ed76fa09e5197ffb1350ff1e6 + with: + githubappId: ${{ vars.GRIDSUITE_ACTIONS_APPID }} + versionType: ${{ github.event.inputs.versionType }} + secrets: + VERSIONBUMP_GHAPP_PRIVATE_KEY: ${{ secrets.VERSIONBUMP_GHAPP_PRIVATE_KEY }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4d51f69 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# VSCode +/.vscode +/.settings +.classpath +.factorypath +.project + +target/ + +# IntelliJ +/.idea +*.iml \ No newline at end of file diff --git a/.mvn/lombok-config-copy.marker b/.mvn/lombok-config-copy.marker new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index 4b0cccf..d955463 100644 --- a/README.md +++ b/README.md @@ -1 +1,7 @@ -# computation \ No newline at end of file +# computation + +[![Actions Status](https://github.com/gridsuite/computation/workflows/CI/badge.svg)](https://github.com/gridsuite/computation/actions) +[![Coverage Status](https://sonarcloud.io/api/project_badges/measure?project=org.gridsuite%computation&metric=coverage)](https://sonarcloud.io/component_measures?id=org.gridsuite%computation&metric=coverage) +[![MPL-2.0 License](https://img.shields.io/badge/license-MPL_2.0-blue.svg)](https://www.mozilla.org/en-US/MPL/2.0/) + +Shared library for common computation and filter classes. \ No newline at end of file diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000..ae30596 --- /dev/null +++ b/lombok.config @@ -0,0 +1 @@ +import target/configs/powsybl-build-tools.jar!powsybl-build-tools/lombok.config \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..3da4194 --- /dev/null +++ b/pom.xml @@ -0,0 +1,312 @@ + + + + 4.0.0 + + + com.powsybl + powsybl-parent + 23 + + + + org.gridsuite + gridsuite-computation + 1.0.0-SNAPSHOT + + jar + Computation library + A shared library for common computation and filter classes + http://www.gridsuite.org/ + + + scm:git:https://github.com/gridsuite/computation.git + scm:git:https://github.com/gridsuite/computation.git + https://github.com/gridsuite/computation + + + + + Rehili Ghazoua + ghazoua.rehili_externe@rte-france.com + RTE + http://www.rte-france.com + + + + + 3.3.3 + 2023.0.1 + 6.1.12 + + 1.18.34 + 2.15.2 + 3.0.2 + 3.1.0 + + 4.4 + + 5.10.2 + 2.2 + 5.11.0 + 3.24.2 + 1.5.1 + + 2025.0.1 + 1.27.2 + + 1.6.0-SNAPSHOT + + 1.9.22.1 + 1.13.3 + 4.1.1 + 3.3.3 + + gridsuite + org.gridsuite:computation + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + + + + + + com.powsybl + powsybl-dependencies + ${powsybl-dependencies.version} + pom + import + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + true + + + + + org.apache.commons + commons-collections4 + ${org-apache-commons.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.junit.jupiter + junit-jupiter-params + ${junit.jupiter.version} + + + org.hamcrest + hamcrest + ${org.hamcrest.version} + + + org.mockito + mockito-core + ${org.mockito.version} + + + org.assertj + assertj-core + ${assertj.version} + + + + + + + + + + + org.apache.commons + commons-collections4 + + + + + com.fasterxml.jackson.core + jackson-databind + + + + + jakarta.validation + jakarta.validation-api + ${jakarta.validation.version} + + + jakarta.persistence + jakarta.persistence-api + ${jakarta.persistence.version} + + + + + org.springframework + spring-core + ${spring.version} + + + org.springframework + spring-context + ${spring.version} + + + org.springframework + spring-messaging + ${spring.version} + + + + + org.springframework.data + spring-data-jpa + ${spring-data-jpa.version} + + + + + org.springframework.cloud + spring-cloud-stream + ${spring-cloud-stream.version} + + + + + org.aspectj + aspectjweaver + ${aspectj.version} + true + + + + + io.micrometer + micrometer-core + ${micrometer.version} + true + + + + + com.powsybl + powsybl-network-store-client + ${powsybl-network-store-client.version} + true + + + com.powsybl + powsybl-security-analysis-api + true + + + + + org.gridsuite + gridsuite-filter + ${gridsuite-filter.version} + + + + + + + + org.projectlombok + lombok + provided + + + + + + + + + org.junit.jupiter + junit-jupiter + test + + + + + org.mockito + mockito-core + test + + + org.mockito + mockito-junit-jupiter + ${org.mockito.version} + test + + + + + org.assertj + assertj-core + test + + + + + org.hamcrest + hamcrest + test + + + + + org.skyscreamer + jsonassert + ${jsonassert.version} + test + + + + + org.springframework.boot + spring-boot-test-autoconfigure + ${spring-boot.version} + test + + + diff --git a/src/main/java/org/gridsuite/computation/ComputationConfig.java b/src/main/java/org/gridsuite/computation/ComputationConfig.java new file mode 100644 index 0000000..efab950 --- /dev/null +++ b/src/main/java/org/gridsuite/computation/ComputationConfig.java @@ -0,0 +1,23 @@ +/** + * 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 com.fasterxml.jackson.databind.InjectableValues; +import com.powsybl.commons.report.ReportNodeDeserializer; +import com.powsybl.commons.report.ReportNodeJsonModule; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ComputationConfig { + @Bean + public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { + return builder -> builder.modulesToInstall(new ReportNodeJsonModule()) + .postConfigurer(objMapper -> objMapper.setInjectableValues(new InjectableValues.Std().addValue(ReportNodeDeserializer.DICTIONARY_VALUE_ID, null))); + } +} diff --git a/src/main/java/org/gridsuite/computation/ComputationException.java b/src/main/java/org/gridsuite/computation/ComputationException.java new file mode 100644 index 0000000..c19157f --- /dev/null +++ b/src/main/java/org/gridsuite/computation/ComputationException.java @@ -0,0 +1,63 @@ +/** + * 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/dto/GlobalFilter.java b/src/main/java/org/gridsuite/computation/dto/GlobalFilter.java new file mode 100644 index 0000000..be33412 --- /dev/null +++ b/src/main/java/org/gridsuite/computation/dto/GlobalFilter.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024, 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.dto; + +import com.powsybl.iidm.network.Country; +import com.powsybl.security.LimitViolationType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * @author maissa Souissi + */ +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class GlobalFilter { + List nominalV; + + List countryCode; + + List genericFilter; + + List limitViolationsTypes; + + Map> substationProperty; +} diff --git a/src/main/java/org/gridsuite/computation/dto/ReportInfos.java b/src/main/java/org/gridsuite/computation/dto/ReportInfos.java new file mode 100644 index 0000000..de2bc2d --- /dev/null +++ b/src/main/java/org/gridsuite/computation/dto/ReportInfos.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2024, 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.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +import java.util.UUID; + +/** + * @author Florent MILLOT + */ +@Builder +@Schema(description = "Report infos") +public record ReportInfos( + UUID reportUuid, + String reporterId, + String computationType +) { +} diff --git a/src/main/java/org/gridsuite/computation/dto/ResourceFilterDTO.java b/src/main/java/org/gridsuite/computation/dto/ResourceFilterDTO.java new file mode 100644 index 0000000..dccb3fd --- /dev/null +++ b/src/main/java/org/gridsuite/computation/dto/ResourceFilterDTO.java @@ -0,0 +1,51 @@ +/** + * 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.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; + +/** + * An object that can be used to filter data with the JPA Criteria API (via Spring Specification) + * @param dataType the type of data we want to filter (text, number) + * @param type the type of filter (contains, startsWith...) + * @param value the value of the filter + * @param column the column / field on which the filter will be applied + * @param tolerance precision/tolerance used for the comparisons (simulates the rounding of the database values) Only useful for numbers. + * @author Kevin Le Saulnier + */ +public record ResourceFilterDTO(@NotNull DataType dataType, @NotNull Type type, Object value, @NotNull String column, Double tolerance) { + + public ResourceFilterDTO(@NotNull DataType dataType, @NotNull Type type, Object value, @NotNull String column) { + this(dataType, type, value, column, null); + } + + public enum DataType { + @JsonProperty("text") + TEXT, + @JsonProperty("number") + NUMBER, + } + + public enum Type { + @JsonProperty("contains") + CONTAINS, + @JsonProperty("startsWith") + STARTS_WITH, + @JsonProperty("equals") + EQUALS, + @JsonProperty("notEqual") + NOT_EQUAL, + @JsonProperty("lessThanOrEqual") + LESS_THAN_OR_EQUAL, + @JsonProperty("greaterThanOrEqual") + GREATER_THAN_OR_EQUAL, + @JsonProperty("in") + IN + } +} + diff --git a/src/main/java/org/gridsuite/computation/service/AbstractComputationObserver.java b/src/main/java/org/gridsuite/computation/service/AbstractComputationObserver.java new file mode 100644 index 0000000..c645bfd --- /dev/null +++ b/src/main/java/org/gridsuite/computation/service/AbstractComputationObserver.java @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2024, 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.service; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NonNull; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author Mathieu Deharbe + * @param powsybl Result class specific to the computation + * @param

powsybl and gridsuite parameters specifics to the computation + */ +@Getter(AccessLevel.PROTECTED) +public abstract class AbstractComputationObserver { + protected static final String OBSERVATION_PREFIX = "app.computation."; + protected static final String PROVIDER_TAG_NAME = "provider"; + protected static final String TYPE_TAG_NAME = "type"; + protected static final String STATUS_TAG_NAME = "status"; + protected static final String COMPUTATION_TOTAL_COUNTER_NAME = OBSERVATION_PREFIX + "count"; + protected static final String COMPUTATION_CURRENT_COUNTER_NAME = OBSERVATION_PREFIX + "current.count"; + + private final ObservationRegistry observationRegistry; + private final MeterRegistry meterRegistry; + + private final Map currentComputationsCount = new ConcurrentHashMap<>(); + + protected AbstractComputationObserver(@NonNull ObservationRegistry observationRegistry, @NonNull MeterRegistry meterRegistry) { + this.observationRegistry = observationRegistry; + this.meterRegistry = meterRegistry; + } + + protected abstract String getComputationType(); + + protected Observation createObservation(String name, AbstractComputationRunContext

runContext) { + Observation observation = Observation.createNotStarted(OBSERVATION_PREFIX + name, observationRegistry) + .lowCardinalityKeyValue(TYPE_TAG_NAME, getComputationType()); + if (runContext.getProvider() != null) { + observation.lowCardinalityKeyValue(PROVIDER_TAG_NAME, runContext.getProvider()); + } + return observation; + } + + public void observe(String name, AbstractComputationRunContext

runContext, Observation.CheckedRunnable callable) throws E { + createObservation(name, runContext).observeChecked(callable); + } + + public T observe(String name, AbstractComputationRunContext

runContext, Observation.CheckedCallable callable) throws E { + return createObservation(name, runContext).observeChecked(callable); + } + + public T observeRun( + String name, AbstractComputationRunContext

runContext, Observation.CheckedCallable callable) throws E { + T result; + try { + incrementCurrentCount(runContext.getProvider()); + result = createObservation(name, runContext).observeChecked(callable); + } finally { + decrementCurrentCount(runContext.getProvider()); + } + incrementTotalCount(runContext, result); + return result; + } + + private void incrementCurrentCount(String provider) { + currentComputationsCount.compute(provider, (k, v) -> (v == null) ? 1 : v + 1); + updateCurrentCountMetric(provider); + } + + private void decrementCurrentCount(String provider) { + currentComputationsCount.compute(provider, (k, v) -> v > 1 ? v - 1 : 0); + updateCurrentCountMetric(provider); + } + + private void updateCurrentCountMetric(String provider) { + Gauge.builder(COMPUTATION_CURRENT_COUNTER_NAME, () -> currentComputationsCount.get(provider)) + .tag(TYPE_TAG_NAME, getComputationType()) + .tag(PROVIDER_TAG_NAME, provider) + .register(meterRegistry); + } + + private void incrementTotalCount(AbstractComputationRunContext

runContext, R result) { + Counter.Builder builder = + Counter.builder(COMPUTATION_TOTAL_COUNTER_NAME); + if (runContext.getProvider() != null) { + builder.tag(PROVIDER_TAG_NAME, runContext.getProvider()); + } + builder.tag(TYPE_TAG_NAME, getComputationType()) + .tag(STATUS_TAG_NAME, getResultStatus(result)) + .register(meterRegistry) + .increment(); + } + + protected abstract String getResultStatus(R res); +} diff --git a/src/main/java/org/gridsuite/computation/service/AbstractComputationResultService.java b/src/main/java/org/gridsuite/computation/service/AbstractComputationResultService.java new file mode 100644 index 0000000..c5424d6 --- /dev/null +++ b/src/main/java/org/gridsuite/computation/service/AbstractComputationResultService.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2024, 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.service; + +import java.util.List; +import java.util.UUID; + +/** + * @author Mathieu Deharbe + * @param status specific to the computation + */ +public abstract class AbstractComputationResultService { + + public abstract void insertStatus(List resultUuids, S status); + + public abstract void delete(UUID resultUuid); + + public abstract void deleteAll(); + + public abstract S findStatus(UUID resultUuid); +} diff --git a/src/main/java/org/gridsuite/computation/service/AbstractComputationRunContext.java b/src/main/java/org/gridsuite/computation/service/AbstractComputationRunContext.java new file mode 100644 index 0000000..8cb4ca9 --- /dev/null +++ b/src/main/java/org/gridsuite/computation/service/AbstractComputationRunContext.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2024, 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.service; + +import com.powsybl.commons.report.ReportNode; +import com.powsybl.iidm.network.Network; +import lombok.Getter; +import lombok.Setter; +import org.gridsuite.computation.dto.ReportInfos; + +import java.util.UUID; + +/** + * @author Mathieu Deharbe + * @param

parameters structure specific to the computation + */ +@Getter +@Setter +public abstract class AbstractComputationRunContext

{ + private final UUID networkUuid; + private final String variantId; + private final String receiver; + private final ReportInfos reportInfos; + private final String userId; + private String provider; + private P parameters; + private ReportNode reportNode; + private Network network; + + protected AbstractComputationRunContext(UUID networkUuid, String variantId, String receiver, ReportInfos reportInfos, + String userId, String provider, P parameters) { + this.networkUuid = networkUuid; + this.variantId = variantId; + this.receiver = receiver; + this.reportInfos = reportInfos; + this.userId = userId; + this.provider = provider; + this.parameters = parameters; + this.reportNode = ReportNode.NO_OP; + this.network = null; + } +} diff --git a/src/main/java/org/gridsuite/computation/service/AbstractComputationService.java b/src/main/java/org/gridsuite/computation/service/AbstractComputationService.java new file mode 100644 index 0000000..824f1ee --- /dev/null +++ b/src/main/java/org/gridsuite/computation/service/AbstractComputationService.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2024, 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.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import org.springframework.util.CollectionUtils; + +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +/** + * @author Mathieu Deharbe + * @param run context specific to a computation, including parameters + * @param run service specific to a computation + * @param enum status specific to a computation + */ +public abstract class AbstractComputationService, T extends AbstractComputationResultService, S> { + + protected ObjectMapper objectMapper; + protected NotificationService notificationService; + protected UuidGeneratorService uuidGeneratorService; + protected T resultService; + @Getter + private final String defaultProvider; + + protected AbstractComputationService(NotificationService notificationService, + T resultService, + ObjectMapper objectMapper, + UuidGeneratorService uuidGeneratorService, + String defaultProvider) { + this.notificationService = Objects.requireNonNull(notificationService); + this.objectMapper = objectMapper; + this.uuidGeneratorService = Objects.requireNonNull(uuidGeneratorService); + this.defaultProvider = defaultProvider; + this.resultService = Objects.requireNonNull(resultService); + } + + public void stop(UUID resultUuid, String receiver) { + notificationService.sendCancelMessage(new CancelContext(resultUuid, receiver).toMessage()); + } + + public void stop(UUID resultUuid, String receiver, String userId) { + notificationService.sendCancelMessage(new CancelContext(resultUuid, receiver, userId).toMessage()); + } + + public abstract List getProviders(); + + public abstract UUID runAndSaveResult(C runContext); + + public void deleteResult(UUID resultUuid) { + resultService.delete(resultUuid); + } + + public void deleteResults(List resultUuids) { + if (!CollectionUtils.isEmpty(resultUuids)) { + resultUuids.forEach(resultService::delete); + } else { + deleteResults(); + } + } + + public void deleteResults() { + resultService.deleteAll(); + } + + public void setStatus(List resultUuids, S status) { + resultService.insertStatus(resultUuids, status); + } + + public S getStatus(UUID resultUuid) { + return resultService.findStatus(resultUuid); + } +} diff --git a/src/main/java/org/gridsuite/computation/service/AbstractFilterService.java b/src/main/java/org/gridsuite/computation/service/AbstractFilterService.java new file mode 100644 index 0000000..6558774 --- /dev/null +++ b/src/main/java/org/gridsuite/computation/service/AbstractFilterService.java @@ -0,0 +1,400 @@ +/** + * 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.service; + +import com.powsybl.commons.PowsyblException; +import com.powsybl.iidm.network.Country; +import com.powsybl.iidm.network.Network; +import com.powsybl.iidm.network.TwoSides; +import com.powsybl.network.store.client.NetworkStoreService; +import com.powsybl.network.store.client.PreloadingStrategy; +import lombok.NonNull; +import org.apache.commons.collections4.CollectionUtils; +import org.gridsuite.computation.dto.GlobalFilter; +import org.gridsuite.computation.dto.ResourceFilterDTO; +import org.gridsuite.filter.AbstractFilter; +import org.gridsuite.filter.FilterLoader; +import org.gridsuite.filter.expertfilter.ExpertFilter; +import org.gridsuite.filter.expertfilter.expertrule.*; +import org.gridsuite.filter.identifierlistfilter.IdentifiableAttributes; +import org.gridsuite.filter.utils.EquipmentType; +import org.gridsuite.filter.utils.FilterServiceUtils; +import org.gridsuite.filter.utils.expertfilter.CombinatorType; +import org.gridsuite.filter.utils.expertfilter.FieldType; +import org.gridsuite.filter.utils.expertfilter.OperatorType; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author Rehili Ghazwa + */ +public abstract class AbstractFilterService implements FilterLoader { + protected static final String FILTERS_NOT_FOUND = "Filters not found"; + protected static final String FILTER_API_VERSION = "v1"; + protected static final String DELIMITER = "/"; + + protected final RestTemplate restTemplate = new RestTemplate(); + protected final NetworkStoreService networkStoreService; + protected final String filterServerBaseUri; + public static final String NETWORK_UUID = "networkUuid"; + + public static final String IDS = "ids"; + + protected AbstractFilterService(NetworkStoreService networkStoreService, String filterServerBaseUri) { + this.networkStoreService = networkStoreService; + this.filterServerBaseUri = filterServerBaseUri; + } + + @Override + public List getFilters(List filtersUuids) { + if (CollectionUtils.isEmpty(filtersUuids)) { + return List.of(); + } + + String ids = filtersUuids.stream() + .map(UUID::toString) + .collect(Collectors.joining(",")); + + String path = UriComponentsBuilder + .fromPath(DELIMITER + FILTER_API_VERSION + "/filters/metadata") + .queryParam(IDS, ids) + .buildAndExpand() + .toUriString(); + + try { + return restTemplate.exchange( + filterServerBaseUri + path, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() { } + ).getBody(); + } catch (HttpStatusCodeException e) { + throw new PowsyblException(FILTERS_NOT_FOUND + " [" + filtersUuids + "]"); + } + } + + protected Network getNetwork(UUID networkUuid, String variantId) { + try { + Network network = networkStoreService.getNetwork(networkUuid, PreloadingStrategy.COLLECTION); + network.getVariantManager().setWorkingVariant(variantId); + return network; + } catch (PowsyblException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); + } + } + + protected List filterNetwork(AbstractFilter filter, Network network) { + return FilterServiceUtils.getIdentifiableAttributes(filter, network, this) + .stream() + .map(IdentifiableAttributes::getId) + .toList(); + } + + public Optional getResourceFilter(@NonNull UUID networkUuid, @NonNull String variantId, @NonNull GlobalFilter globalFilter, + List equipmentTypes, String columnName) { + + Network network = getNetwork(networkUuid, variantId); + List genericFilters = getFilters(globalFilter.getGenericFilter()); + + // Filter equipments by type + Map> subjectIdsByEquipmentType = filterEquipmentsByType( + network, globalFilter, genericFilters, equipmentTypes + ); + + // Combine all results into one list + List subjectIds = subjectIdsByEquipmentType.values().stream() + .filter(Objects::nonNull) + .flatMap(List::stream) + .toList(); + + return subjectIds.isEmpty() ? Optional.empty() : + Optional.of(new ResourceFilterDTO( + ResourceFilterDTO.DataType.TEXT, + ResourceFilterDTO.Type.IN, + subjectIds, + columnName + )); + } + + protected List createNumberExpertRules(List values, FieldType fieldType) { + List rules = new ArrayList<>(); + if (values != null) { + for (String value : values) { + rules.add(NumberExpertRule.builder() + .value(Double.valueOf(value)) + .field(fieldType) + .operator(OperatorType.EQUALS) + .build()); + } + } + return rules; + } + + protected AbstractExpertRule createPropertiesRule(String property, List propertiesValues, FieldType fieldType) { + return PropertiesExpertRule.builder() + .combinator(CombinatorType.OR) + .operator(OperatorType.IN) + .field(fieldType) + .propertyName(property) + .propertyValues(propertiesValues) + .build(); + } + + protected List createEnumExpertRules(List values, FieldType fieldType) { + List rules = new ArrayList<>(); + if (values != null) { + for (Country value : values) { + rules.add(EnumExpertRule.builder() + .value(value.toString()) + .field(fieldType) + .operator(OperatorType.EQUALS) + .build()); + } + } + return rules; + } + + protected AbstractExpertRule createCombination(CombinatorType combinatorType, List rules) { + return CombinatorExpertRule.builder().combinator(combinatorType).rules(rules).build(); + } + + protected Optional createOrCombination(List rules) { + if (rules.isEmpty()) { + return Optional.empty(); + } + return Optional.of(rules.size() > 1 ? createCombination(CombinatorType.OR, rules) : rules.getFirst()); + } + + /** + * Extracts equipment IDs from a generic filter based on equipment type + */ + protected List extractEquipmentIdsFromGenericFilter( + AbstractFilter filter, + EquipmentType targetEquipmentType, + Network network) { + + if (filter.getEquipmentType() == targetEquipmentType) { + return filterNetwork(filter, network); + } else if (filter.getEquipmentType() == EquipmentType.VOLTAGE_LEVEL) { + ExpertFilter voltageFilter = buildExpertFilterWithVoltageLevelIdsCriteria( + filter.getId(), targetEquipmentType); + return filterNetwork(voltageFilter, network); + } + return List.of(); + } + + /** + * Combines multiple filter results using AND or OR logic + */ + protected List combineFilterResults(List> filterResults, boolean useAndLogic) { + if (filterResults.isEmpty()) { + return List.of(); + } + + if (filterResults.size() == 1) { + return filterResults.getFirst(); + } + + if (useAndLogic) { + // Intersection of all results + Set result = new HashSet<>(filterResults.getFirst()); + for (int i = 1; i < filterResults.size(); i++) { + result.retainAll(filterResults.get(i)); + } + return new ArrayList<>(result); + } else { + // Union of all results + Set result = new HashSet<>(); + filterResults.forEach(result::addAll); + return new ArrayList<>(result); + } + } + + /** + * Extracts filtered equipment IDs by applying expert and generic filters + */ + protected List extractFilteredEquipmentIds( + Network network, + GlobalFilter globalFilter, + List genericFilters, + EquipmentType equipmentType) { + + List> allFilterResults = new ArrayList<>(); + + // Extract IDs from expert filter + ExpertFilter expertFilter = buildExpertFilter(globalFilter, equipmentType); + if (expertFilter != null) { + allFilterResults.add(filterNetwork(expertFilter, network)); + } + + // Extract IDs from generic filters + for (AbstractFilter filter : genericFilters) { + List filterResult = extractEquipmentIdsFromGenericFilter(filter, equipmentType, network); + if (!filterResult.isEmpty()) { + allFilterResults.add(filterResult); + } + } + + // Combine results with appropriate logic + // Expert filters use OR between them, generic filters use AND + return combineFilterResults(allFilterResults, !genericFilters.isEmpty()); + } + + /** + * Builds expert filter with voltage level IDs criteria + */ + protected ExpertFilter buildExpertFilterWithVoltageLevelIdsCriteria(UUID filterUuid, EquipmentType equipmentType) { + AbstractExpertRule voltageLevelId1Rule = createVoltageLevelIdRule(filterUuid, TwoSides.ONE); + AbstractExpertRule voltageLevelId2Rule = createVoltageLevelIdRule(filterUuid, TwoSides.TWO); + AbstractExpertRule orCombination = createCombination(CombinatorType.OR, + List.of(voltageLevelId1Rule, voltageLevelId2Rule)); + return new ExpertFilter(UUID.randomUUID(), new Date(), equipmentType, orCombination); + } + + /** + * Creates voltage level ID rule for filtering + */ + protected AbstractExpertRule createVoltageLevelIdRule(UUID filterUuid, TwoSides side) { + return FilterUuidExpertRule.builder() + .operator(OperatorType.IS_PART_OF) + .field(side == TwoSides.ONE ? FieldType.VOLTAGE_LEVEL_ID_1 : FieldType.VOLTAGE_LEVEL_ID_2) + .values(Set.of(filterUuid.toString())) + .build(); + } + + /** + * Builds all expert rules for a global filter and equipment type + */ + protected List buildAllExpertRules(GlobalFilter globalFilter, EquipmentType equipmentType) { + List andRules = new ArrayList<>(); + + // Nominal voltage rules + buildNominalVoltageRules(globalFilter.getNominalV(), equipmentType) + .ifPresent(andRules::add); + + // Country code rules + buildCountryCodeRules(globalFilter.getCountryCode(), equipmentType) + .ifPresent(andRules::add); + + // Substation property rules + if (globalFilter.getSubstationProperty() != null) { + buildSubstationPropertyRules(globalFilter.getSubstationProperty(), equipmentType) + .ifPresent(andRules::add); + } + + return andRules; + } + + /** + * Builds nominal voltage rules combining all relevant field types + */ + protected Optional buildNominalVoltageRules( + List nominalVoltages, EquipmentType equipmentType) { + + List fieldTypes = getNominalVoltageFieldType(equipmentType); + List rules = fieldTypes.stream() + .flatMap(fieldType -> createNumberExpertRules(nominalVoltages, fieldType).stream()) + .toList(); + + return createOrCombination(rules); + } + + /** + * Builds country code rules combining all relevant field types + */ + protected Optional buildCountryCodeRules( + List countryCodes, EquipmentType equipmentType) { + + List fieldTypes = getCountryCodeFieldType(equipmentType); + List rules = fieldTypes.stream() + .flatMap(fieldType -> createEnumExpertRules(countryCodes, fieldType).stream()) + .toList(); + + return createOrCombination(rules); + } + + /** + * Builds substation property rules combining all relevant field types + */ + protected Optional buildSubstationPropertyRules( + Map> properties, EquipmentType equipmentType) { + + List fieldTypes = getSubstationPropertiesFieldTypes(equipmentType); + List rules = properties.entrySet().stream() + .flatMap(entry -> fieldTypes.stream() + .map(fieldType -> createPropertiesRule( + entry.getKey(), entry.getValue(), fieldType))) + .toList(); + + return createOrCombination(rules); + } + + /** + * Filters equipments by type and returns map of IDs grouped by equipment type + */ + protected Map> filterEquipmentsByType( + Network network, + GlobalFilter globalFilter, + List genericFilters, + List equipmentTypes) { + + Map> result = new EnumMap<>(EquipmentType.class); + + for (EquipmentType equipmentType : equipmentTypes) { + List filteredIds = extractFilteredEquipmentIds(network, globalFilter, genericFilters, equipmentType); + if (!filteredIds.isEmpty()) { + result.put(equipmentType, filteredIds); + } + } + + return result; + } + + /** + * Builds expert filter from global filter and equipment type + */ + protected ExpertFilter buildExpertFilter(GlobalFilter globalFilter, EquipmentType equipmentType) { + List andRules = buildAllExpertRules(globalFilter, equipmentType); + + return andRules.isEmpty() ? null : + new ExpertFilter(UUID.randomUUID(), new Date(), equipmentType, + createCombination(CombinatorType.AND, andRules)); + } + + protected List getNominalVoltageFieldType(EquipmentType equipmentType) { + return switch (equipmentType) { + case LINE, TWO_WINDINGS_TRANSFORMER -> List.of(FieldType.NOMINAL_VOLTAGE_1, FieldType.NOMINAL_VOLTAGE_2); + case VOLTAGE_LEVEL -> List.of(FieldType.NOMINAL_VOLTAGE); + default -> List.of(); + }; + } + + protected List getCountryCodeFieldType(EquipmentType equipmentType) { + return switch (equipmentType) { + case VOLTAGE_LEVEL, TWO_WINDINGS_TRANSFORMER -> List.of(FieldType.COUNTRY); + case LINE -> List.of(FieldType.COUNTRY_1, FieldType.COUNTRY_2); + default -> List.of(); + }; + } + + protected List getSubstationPropertiesFieldTypes(EquipmentType equipmentType) { + return equipmentType == EquipmentType.LINE ? + List.of(FieldType.SUBSTATION_PROPERTIES_1, FieldType.SUBSTATION_PROPERTIES_2) : + List.of(FieldType.SUBSTATION_PROPERTIES); + } +} + + + diff --git a/src/main/java/org/gridsuite/computation/service/AbstractResultContext.java b/src/main/java/org/gridsuite/computation/service/AbstractResultContext.java new file mode 100644 index 0000000..3068a89 --- /dev/null +++ b/src/main/java/org/gridsuite/computation/service/AbstractResultContext.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2024, 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.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Data; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; + +import java.io.UncheckedIOException; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +import static org.gridsuite.computation.service.NotificationService.*; + +/** + * @author Mathieu Deharbe + * @param run context specific to a computation, including parameters + */ +@Data +public abstract class AbstractResultContext> { + + protected static final String RESULT_UUID_HEADER = "resultUuid"; + + protected static final String NETWORK_UUID_HEADER = "networkUuid"; + + protected static final String REPORT_UUID_HEADER = "reportUuid"; + + public static final String VARIANT_ID_HEADER = "variantId"; + + public static final String REPORTER_ID_HEADER = "reporterId"; + + public static final String REPORT_TYPE_HEADER = "reportType"; + + protected static final String MESSAGE_ROOT_NAME = "parameters"; + + private final UUID resultUuid; + private final C runContext; + + protected AbstractResultContext(UUID resultUuid, C runContext) { + this.resultUuid = Objects.requireNonNull(resultUuid); + this.runContext = Objects.requireNonNull(runContext); + } + + public Message toMessage(ObjectMapper objectMapper) { + String parametersJson = ""; + if (objectMapper != null) { + try { + parametersJson = objectMapper.writeValueAsString(runContext.getParameters()); + } catch (JsonProcessingException e) { + throw new UncheckedIOException(e); + } + } + return MessageBuilder.withPayload(parametersJson) + .setHeader(RESULT_UUID_HEADER, resultUuid.toString()) + .setHeader(NETWORK_UUID_HEADER, runContext.getNetworkUuid().toString()) + .setHeader(VARIANT_ID_HEADER, runContext.getVariantId()) + .setHeader(HEADER_RECEIVER, runContext.getReceiver()) + .setHeader(HEADER_PROVIDER, runContext.getProvider()) + .setHeader(HEADER_USER_ID, runContext.getUserId()) + .setHeader(REPORT_UUID_HEADER, runContext.getReportInfos().reportUuid() != null ? runContext.getReportInfos().reportUuid().toString() : null) + .setHeader(REPORTER_ID_HEADER, runContext.getReportInfos().reporterId()) + .setHeader(REPORT_TYPE_HEADER, runContext.getReportInfos().computationType()) + .copyHeaders(getSpecificMsgHeaders(objectMapper)) + .build(); + } + + protected Map getSpecificMsgHeaders(ObjectMapper ignoredObjectMapper) { + return Map.of(); + } +} diff --git a/src/main/java/org/gridsuite/computation/service/AbstractWorkerService.java b/src/main/java/org/gridsuite/computation/service/AbstractWorkerService.java new file mode 100644 index 0000000..45d5c99 --- /dev/null +++ b/src/main/java/org/gridsuite/computation/service/AbstractWorkerService.java @@ -0,0 +1,260 @@ +/** + * Copyright (c) 2024, 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.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.powsybl.commons.PowsyblException; +import com.powsybl.commons.report.ReportNode; +import com.powsybl.iidm.network.Network; +import com.powsybl.iidm.network.VariantManagerConstants; +import com.powsybl.network.store.client.NetworkStoreService; +import com.powsybl.network.store.client.PreloadingStrategy; +import org.apache.commons.lang3.StringUtils; +import org.gridsuite.computation.ComputationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.messaging.Message; +import org.springframework.web.server.ResponseStatusException; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; + +/** + * @author Mathieu Deharbe + * @param powsybl Result class specific to the computation + * @param Run context specific to a computation, including parameters + * @param

powsybl and gridsuite Parameters specifics to the computation + * @param result service specific to the computation + */ +public abstract class AbstractWorkerService, P, S extends AbstractComputationResultService> { + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractWorkerService.class); + + protected final Lock lockRunAndCancel = new ReentrantLock(); + protected final ObjectMapper objectMapper; + protected final NetworkStoreService networkStoreService; + protected final ReportService reportService; + protected final ExecutionService executionService; + protected final NotificationService notificationService; + protected final AbstractComputationObserver observer; + protected final Map> futures = new ConcurrentHashMap<>(); + protected final Map cancelComputationRequests = new ConcurrentHashMap<>(); + protected final S resultService; + + protected AbstractWorkerService(NetworkStoreService networkStoreService, + NotificationService notificationService, + ReportService reportService, + S resultService, + ExecutionService executionService, + AbstractComputationObserver observer, + ObjectMapper objectMapper) { + this.networkStoreService = networkStoreService; + this.notificationService = notificationService; + this.reportService = reportService; + this.resultService = resultService; + this.executionService = executionService; + this.observer = observer; + this.objectMapper = objectMapper; + } + + protected PreloadingStrategy getNetworkPreloadingStrategy() { + return PreloadingStrategy.COLLECTION; + } + + protected Network getNetwork(UUID networkUuid, String variantId) { + try { + Network network = networkStoreService.getNetwork(networkUuid, getNetworkPreloadingStrategy()); + String variant = StringUtils.isBlank(variantId) ? VariantManagerConstants.INITIAL_VARIANT_ID : variantId; + network.getVariantManager().setWorkingVariant(variant); + return network; + } catch (PowsyblException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); + } + } + + protected void cleanResultsAndPublishCancel(UUID resultUuid, String receiver) { + resultService.delete(resultUuid); + notificationService.publishStop(resultUuid, receiver, getComputationType()); + if (LOGGER.isInfoEnabled()) { + LOGGER.info("{} (resultUuid='{}')", + NotificationService.getCancelMessage(getComputationType()), + resultUuid); + } + } + + private boolean cancelAsync(CancelContext cancelContext) { + lockRunAndCancel.lock(); + boolean isCanceled = false; + try { + cancelComputationRequests.put(cancelContext.resultUuid(), cancelContext); + + // find the completableFuture associated with result uuid + CompletableFuture future = futures.get(cancelContext.resultUuid()); + if (future != null) { + isCanceled = future.cancel(true); // cancel computation in progress + if (isCanceled) { + cleanResultsAndPublishCancel(cancelContext.resultUuid(), cancelContext.receiver()); + } + } + } finally { + lockRunAndCancel.unlock(); + } + return isCanceled; + } + + protected abstract AbstractResultContext fromMessage(Message message); + + protected boolean resultCanBeSaved(R result) { + return result != null; + } + + public Consumer> consumeRun() { + return message -> { + AbstractResultContext resultContext = fromMessage(message); + AtomicReference rootReporter = new AtomicReference<>(ReportNode.NO_OP); + try { + Network network = getNetwork(resultContext.getRunContext().getNetworkUuid(), + resultContext.getRunContext().getVariantId()); + resultContext.getRunContext().setNetwork(network); + observer.observe("global.run", resultContext.getRunContext(), () -> { + long startTime = System.nanoTime(); + R result = run(resultContext.getRunContext(), resultContext.getResultUuid(), rootReporter); + + LOGGER.info("Just run in {}s", TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startTime)); + + if (resultCanBeSaved(result)) { + startTime = System.nanoTime(); + observer.observe("results.save", resultContext.getRunContext(), () -> saveResult(network, resultContext, result)); + + LOGGER.info("Stored in {}s", TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startTime)); + + sendResultMessage(resultContext, result); + LOGGER.info("{} complete (resultUuid='{}')", getComputationType(), resultContext.getResultUuid()); + } + }); + } catch (CancellationException e) { + // Do nothing + } 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()); + } finally { + clean(resultContext); + } + }; + } + + /** + * Perform cleaning + * @param resultContext The context of the computation + */ + protected void clean(AbstractResultContext resultContext) { + futures.remove(resultContext.getResultUuid()); + cancelComputationRequests.remove(resultContext.getResultUuid()); + } + + /** + * Handle exception in consumeRun that is not a CancellationException + * @param resultContext The context of the computation + * @param exception The exception to handle + */ + protected void handleNonCancellationException(AbstractResultContext resultContext, Exception exception, AtomicReference rootReporter) { + } + + public Consumer> consumeCancel() { + return message -> { + CancelContext cancelContext = CancelContext.fromMessage(message); + boolean isCancelled = cancelAsync(cancelContext); + if (!isCancelled) { + notificationService.publishCancelFailed(cancelContext.resultUuid(), cancelContext.receiver(), getComputationType(), cancelContext.userId()); + } + }; + } + + protected abstract void saveResult(Network network, AbstractResultContext resultContext, R result); + + protected void sendResultMessage(AbstractResultContext resultContext, R ignoredResult) { + notificationService.sendResultMessage(resultContext.getResultUuid(), resultContext.getRunContext().getReceiver(), + resultContext.getRunContext().getUserId(), null); + } + + /** + * Do some extra task before running the computation, e.g. print log or init extra data for the run context + * @param ignoredRunContext This context may be used for further computation in overriding classes + */ + protected void preRun(C ignoredRunContext) { + LOGGER.info("Run {} computation...", getComputationType()); + } + + protected R run(C runContext, UUID resultUuid, AtomicReference rootReporter) { + String provider = runContext.getProvider(); + ReportNode reportNode = ReportNode.NO_OP; + + if (runContext.getReportInfos() != null && runContext.getReportInfos().reportUuid() != null) { + final String reportType = runContext.getReportInfos().computationType(); + String rootReporterId = runContext.getReportInfos().reporterId(); + ReportNode rootReporterNode = ReportNode.newRootReportNode() + .withAllResourceBundlesFromClasspath() + .withMessageTemplate("ws.commons.rootReporterId") + .withUntypedValue("rootReporterId", rootReporterId).build(); + rootReporter.set(rootReporterNode); + reportNode = rootReporter.get().newReportNode().withMessageTemplate("ws.commons.reportType") + .withUntypedValue("reportType", reportType) + .withUntypedValue("optionalProvider", provider != null ? " (" + provider + ")" : "").add(); + // Delete any previous computation logs + observer.observe("report.delete", + runContext, () -> reportService.deleteReport(runContext.getReportInfos().reportUuid())); + } + runContext.setReportNode(reportNode); + + preRun(runContext); + CompletableFuture future = runAsync(runContext, provider, resultUuid); + R result = future == null ? null : observer.observeRun("run", runContext, future::join); + postRun(runContext, rootReporter, result); + return result; + } + + /** + * Do some extra task after running the computation + * @param runContext This context may be used for extra task in overriding classes + * @param rootReportNode root of the reporter tree + * @param ignoredResult The result of the computation + */ + protected void postRun(C runContext, AtomicReference rootReportNode, R ignoredResult) { + if (runContext.getReportInfos().reportUuid() != null) { + observer.observe("report.send", runContext, () -> reportService.sendReport(runContext.getReportInfos().reportUuid(), rootReportNode.get())); + } + } + + protected CompletableFuture runAsync( + C runContext, + String provider, + UUID resultUuid) { + lockRunAndCancel.lock(); + try { + if (resultUuid != null && cancelComputationRequests.get(resultUuid) != null) { + return null; + } + CompletableFuture future = getCompletableFuture(runContext, provider, resultUuid); + if (resultUuid != null) { + futures.put(resultUuid, future); + } + return future; + } finally { + lockRunAndCancel.unlock(); + } + } + + protected abstract String getComputationType(); + + protected abstract CompletableFuture getCompletableFuture(C runContext, String provider, UUID resultUuid); +} diff --git a/src/main/java/org/gridsuite/computation/service/CancelContext.java b/src/main/java/org/gridsuite/computation/service/CancelContext.java new file mode 100644 index 0000000..e343b8f --- /dev/null +++ b/src/main/java/org/gridsuite/computation/service/CancelContext.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2024, 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.service; + +import org.gridsuite.computation.utils.MessageUtils; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.MessageBuilder; + +import java.util.Objects; +import java.util.UUID; + +import static org.gridsuite.computation.service.NotificationService.*; + +/** + * @author Anis Touri + */ +public record CancelContext(UUID resultUuid, String receiver, String userId) { + + public CancelContext(UUID resultUuid, String receiver, String userId) { + this.resultUuid = Objects.requireNonNull(resultUuid); + this.receiver = Objects.requireNonNull(receiver); + this.userId = userId; + } + + public CancelContext(UUID resultUuid, String receiver) { + this(resultUuid, receiver, null); + } + + public static CancelContext fromMessage(Message message) { + Objects.requireNonNull(message); + MessageHeaders headers = message.getHeaders(); + UUID resultUuid = UUID.fromString(MessageUtils.getNonNullHeader(headers, HEADER_RESULT_UUID)); + String receiver = headers.get(HEADER_RECEIVER, String.class); + String userId = headers.get(HEADER_USER_ID, String.class); + return new CancelContext(resultUuid, receiver, userId); + } + + public Message toMessage() { + return MessageBuilder.withPayload("") + .setHeader(HEADER_RESULT_UUID, resultUuid.toString()) + .setHeader(HEADER_RECEIVER, receiver) + .setHeader(HEADER_USER_ID, userId) + .build(); + } +} diff --git a/src/main/java/org/gridsuite/computation/service/ExecutionService.java b/src/main/java/org/gridsuite/computation/service/ExecutionService.java new file mode 100644 index 0000000..9e97db8 --- /dev/null +++ b/src/main/java/org/gridsuite/computation/service/ExecutionService.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2024, 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.service; + +import com.powsybl.computation.ComputationManager; +import com.powsybl.computation.local.LocalComputationManager; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.Getter; +import lombok.SneakyThrows; +import org.springframework.stereotype.Service; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + + +/** + * @author David Braquart + */ +@Service +@Getter +public class ExecutionService { + + private ExecutorService executorService; + + private ComputationManager computationManager; + + @SneakyThrows + @PostConstruct + private void postConstruct() { + executorService = Executors.newCachedThreadPool(); + computationManager = new LocalComputationManager(getExecutorService()); + } + + @PreDestroy + private void preDestroy() { + executorService.shutdown(); + } +} diff --git a/src/main/java/org/gridsuite/computation/service/NotificationService.java b/src/main/java/org/gridsuite/computation/service/NotificationService.java new file mode 100644 index 0000000..d3aa71d --- /dev/null +++ b/src/main/java/org/gridsuite/computation/service/NotificationService.java @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2022, 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.service; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.gridsuite.computation.utils.annotations.PostCompletion; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.stream.function.StreamBridge; +import org.springframework.lang.Nullable; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.stereotype.Service; + +import java.util.Map; +import java.util.UUID; + +/** + * @author Etienne Homer message) { + RUN_MESSAGE_LOGGER.debug(SENDING_MESSAGE, message); + publisher.send(publishPrefix + "Run-out-0", message); + } + + public void sendCancelMessage(Message message) { + CANCEL_MESSAGE_LOGGER.debug(SENDING_MESSAGE, message); + publisher.send(publishPrefix + "Cancel-out-0", message); + } + + @PostCompletion + public void sendResultMessage(UUID resultUuid, String receiver, String userId, @Nullable Map additionalHeaders) { + MessageBuilder builder = MessageBuilder + .withPayload("") + .setHeader(HEADER_RESULT_UUID, resultUuid.toString()) + .setHeader(HEADER_RECEIVER, receiver) + .setHeader(HEADER_USER_ID, userId) + .copyHeaders(additionalHeaders); + Message message = builder.build(); + RESULT_MESSAGE_LOGGER.debug(SENDING_MESSAGE, message); + publisher.send(publishPrefix + "Result-out-0", message); + } + + @PostCompletion + public void publishStop(UUID resultUuid, String receiver, String computationLabel) { + Message message = MessageBuilder + .withPayload("") + .setHeader(HEADER_RESULT_UUID, resultUuid.toString()) + .setHeader(HEADER_RECEIVER, receiver) + .setHeader(HEADER_MESSAGE, getCancelMessage(computationLabel)) + .build(); + STOP_MESSAGE_LOGGER.debug(SENDING_MESSAGE, message); + publisher.send(publishPrefix + "Stopped-out-0", message); + } + + @PostCompletion + public void publishCancelFailed(UUID resultUuid, String receiver, String computationLabel, String userId) { + Message message = MessageBuilder + .withPayload("") + .setHeader(HEADER_RESULT_UUID, resultUuid.toString()) + .setHeader(HEADER_RECEIVER, receiver) + .setHeader(HEADER_USER_ID, userId) + .setHeader(HEADER_MESSAGE, getCancelFailedMessage(computationLabel)) + .build(); + CANCEL_FAILED_MESSAGE_LOGGER.info(SENDING_MESSAGE, message); + publisher.send(publishPrefix + "CancelFailed-out-0", message); + } + + public static String getCancelMessage(String computationLabel) { + return computationLabel + " was canceled"; + } + + public static String getFailedMessage(String computationLabel) { + return computationLabel + " has failed"; + } + + public static String getCancelFailedMessage(String computationLabel) { + return computationLabel + " could not be cancelled"; + } +} diff --git a/src/main/java/org/gridsuite/computation/service/ReportService.java b/src/main/java/org/gridsuite/computation/service/ReportService.java new file mode 100644 index 0000000..7ebf468 --- /dev/null +++ b/src/main/java/org/gridsuite/computation/service/ReportService.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2022, 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.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.powsybl.commons.PowsyblException; +import com.powsybl.commons.report.ReportNode; +import lombok.Setter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.Objects; +import java.util.UUID; + +/** + * @author Anis Touri + */ +@Service +public class ReportService { + + static final String REPORT_API_VERSION = "v1"; + private static final String DELIMITER = "/"; + private static final String QUERY_PARAM_REPORT_THROW_ERROR = "errorOnReportNotFound"; + @Setter + private String reportServerBaseUri; + + private final RestTemplate restTemplate; + + private final ObjectMapper objectMapper; + + public ReportService(ObjectMapper objectMapper, + @Value("${gridsuite.services.report-server.base-uri:http://report-server/}") String reportServerBaseUri, + RestTemplate restTemplate) { + this.reportServerBaseUri = reportServerBaseUri; + this.objectMapper = objectMapper; + this.restTemplate = restTemplate; + } + + private String getReportServerURI() { + return this.reportServerBaseUri + DELIMITER + REPORT_API_VERSION + DELIMITER + "reports" + DELIMITER; + } + + public void sendReport(UUID reportUuid, ReportNode reportNode) { + Objects.requireNonNull(reportUuid); + + var path = UriComponentsBuilder.fromPath("{reportUuid}") + .buildAndExpand(reportUuid) + .toUriString(); + var headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + try { + String str = objectMapper.writeValueAsString(reportNode); + restTemplate.exchange(getReportServerURI() + path, HttpMethod.PUT, new HttpEntity<>(str, headers), ReportNode.class); + } catch (JsonProcessingException error) { + throw new PowsyblException("Error sending report", error); + } + } + + public void deleteReport(UUID reportUuid) { + Objects.requireNonNull(reportUuid); + + var path = UriComponentsBuilder.fromPath("{reportUuid}") + .queryParam(QUERY_PARAM_REPORT_THROW_ERROR, false) + .buildAndExpand(reportUuid) + .toUriString(); + var headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + restTemplate.exchange(getReportServerURI() + path, HttpMethod.DELETE, new HttpEntity<>(headers), Void.class); + } +} diff --git a/src/main/java/org/gridsuite/computation/service/UuidGeneratorService.java b/src/main/java/org/gridsuite/computation/service/UuidGeneratorService.java new file mode 100644 index 0000000..70f2ef7 --- /dev/null +++ b/src/main/java/org/gridsuite/computation/service/UuidGeneratorService.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2024, 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.service; + +import org.springframework.stereotype.Service; + +import java.util.UUID; + +/** + * @author Geoffroy Jamgotchian + */ +@Service +public class UuidGeneratorService { + + public UUID generate() { + return UUID.randomUUID(); + } +} diff --git a/src/main/java/org/gridsuite/computation/specification/AbstractCommonSpecificationBuilder.java b/src/main/java/org/gridsuite/computation/specification/AbstractCommonSpecificationBuilder.java new file mode 100644 index 0000000..aba1daf --- /dev/null +++ b/src/main/java/org/gridsuite/computation/specification/AbstractCommonSpecificationBuilder.java @@ -0,0 +1,85 @@ +/** + * 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.specification; + +import jakarta.persistence.criteria.Path; +import jakarta.persistence.criteria.Root; +import lombok.NoArgsConstructor; +import org.gridsuite.computation.dto.ResourceFilterDTO; +import org.gridsuite.computation.utils.SpecificationUtils; +import org.springframework.data.jpa.domain.Specification; + +import java.util.List; +import java.util.UUID; + +/** + * @author Kevin LE SAULNIER + */ +@NoArgsConstructor +public abstract class AbstractCommonSpecificationBuilder { + + public Specification resultUuidEquals(UUID value) { + return (root, cq, cb) -> cb.equal(getResultIdPath(root), value); + } + + public Specification uuidIn(List uuids) { + return (root, cq, cb) -> root.get(getIdFieldName()).in(uuids); + } + + /** + * @param distinct : true if you want to force the results to be distinct. + * Since sql joins generates duplicate results, we may need to use distinct here + * But can't use both distinct and sort on nested field (sql limitation) + */ + public Specification buildSpecification(UUID resultUuid, List resourceFilters, boolean distinct) { + List childrenFilters = resourceFilters != null ? resourceFilters.stream().filter(this::isNotParentFilter).toList() : List.of(); + // filter by resultUuid + Specification specification = Specification.where(resultUuidEquals(resultUuid)); + if (distinct) { + specification = specification.and(SpecificationUtils.distinct()); + } + if (childrenFilters.isEmpty()) { + Specification spec = addSpecificFilterWhenNoChildrenFilter(); + if (spec != null) { + specification = specification.and(spec); + } + } else { + // needed here to filter main entities that would have empty collection when filters are applied + Specification spec = addSpecificFilterWhenChildrenFilters(); + if (spec != null) { + specification = specification.and(spec); + } + } + + return SpecificationUtils.appendFiltersToSpecification(specification, resourceFilters); + } + + public Specification buildSpecification(UUID resultUuid, List resourceFilters) { + return buildSpecification(resultUuid, resourceFilters, true); + } + + public Specification buildLimitViolationsSpecification(List uuids, List resourceFilters) { + List childrenFilters = resourceFilters.stream().filter(this::isNotParentFilter).toList(); + Specification specification = Specification.where(uuidIn(uuids)); + + return SpecificationUtils.appendFiltersToSpecification(specification, childrenFilters); + } + + public Specification addSpecificFilterWhenChildrenFilters() { + return null; + } + + public Specification addSpecificFilterWhenNoChildrenFilter() { + return null; + } + + public abstract boolean isNotParentFilter(ResourceFilterDTO filter); + + public abstract String getIdFieldName(); + + public abstract Path getResultIdPath(Root root); +} diff --git a/src/main/java/org/gridsuite/computation/utils/ComputationResultUtils.java b/src/main/java/org/gridsuite/computation/utils/ComputationResultUtils.java new file mode 100644 index 0000000..6a5ddc4 --- /dev/null +++ b/src/main/java/org/gridsuite/computation/utils/ComputationResultUtils.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2024, 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.utils; + +import com.powsybl.iidm.network.*; +import com.powsybl.security.BusBreakerViolationLocation; +import com.powsybl.security.LimitViolation; +import com.powsybl.security.NodeBreakerViolationLocation; +import com.powsybl.security.ViolationLocation; + +import java.util.*; +import java.util.stream.Collectors; + +import static com.powsybl.iidm.network.IdentifiableType.BUSBAR_SECTION; +import static com.powsybl.security.LimitViolationType.*; +import static com.powsybl.security.ViolationLocation.Type.NODE_BREAKER; + +/** + * @author Jamal KHEYYAD + */ +public final class ComputationResultUtils { + + private ComputationResultUtils() { + } + + public static String getViolationLocationId(LimitViolation limitViolation, Network network) { + // LocationId only for voltage-based limit violations + if (!Set.of(LOW_VOLTAGE, HIGH_VOLTAGE, LOW_VOLTAGE_ANGLE, HIGH_VOLTAGE_ANGLE).contains(limitViolation.getLimitType())) { + return null; + } + + Optional violationLocation = limitViolation.getViolationLocation(); + if (violationLocation.isEmpty()) { + return limitViolation.getSubjectId(); + } + + ViolationLocation location = violationLocation.get(); + if (location.getType() == NODE_BREAKER) { + return getNodeBreakerViolationLocationId((NodeBreakerViolationLocation) location, network); + } else { + return getBusBreakerViolationLocationId((BusBreakerViolationLocation) location, network, limitViolation.getSubjectId()); + } + } + + private static String getNodeBreakerViolationLocationId(NodeBreakerViolationLocation nodeBreakerViolationLocation, Network network) { + VoltageLevel vl = network.getVoltageLevel(nodeBreakerViolationLocation.getVoltageLevelId()); + + List busBarIds = nodeBreakerViolationLocation.getNodes().stream() + .map(node -> vl.getNodeBreakerView().getTerminal(node)) + .filter(Objects::nonNull) + .map(Terminal::getConnectable) + .filter(t -> t.getType() == BUSBAR_SECTION) + .map(Identifiable::getId) + .distinct() + .toList(); + + String busId = null; + if (!busBarIds.isEmpty()) { + busId = getBusId(vl, new HashSet<>(busBarIds)); + } + return formatViolationLocationId(busId != null ? List.of() : busBarIds, busId != null ? busId : nodeBreakerViolationLocation.getVoltageLevelId()); + } + + private static String getBusId(VoltageLevel voltageLevel, Set sjbIds) { + Optional bus = voltageLevel.getBusView() + .getBusStream() + .filter(b -> { + Set busSjbIds = b.getConnectedTerminalStream().map(Terminal::getConnectable).filter(c -> c.getType() == BUSBAR_SECTION).map(Connectable::getId).collect(Collectors.toSet()); + return busSjbIds.equals(sjbIds); + }) + .findFirst(); + return bus.map(Identifiable::getId).orElse(null); + } + + private static String formatViolationLocationId(List elementsIds, String subjectId) { + return !elementsIds.isEmpty() ? + subjectId + " (" + String.join(", ", elementsIds) + ")" : + subjectId; + } + + private static String getBusBreakerViolationLocationId(BusBreakerViolationLocation busBreakerViolationLocation, Network network, String subjectId) { + List busBreakerIds = busBreakerViolationLocation + .getBusView(network) + .getBusStream() + .map(Identifiable::getId) + .distinct() + .toList(); + + return busBreakerIds.size() == 1 ? formatViolationLocationId(List.of(), busBreakerIds.getFirst()) : formatViolationLocationId(busBreakerIds, subjectId); + } + +} diff --git a/src/main/java/org/gridsuite/computation/utils/FilterUtils.java b/src/main/java/org/gridsuite/computation/utils/FilterUtils.java new file mode 100644 index 0000000..b503df5 --- /dev/null +++ b/src/main/java/org/gridsuite/computation/utils/FilterUtils.java @@ -0,0 +1,49 @@ +/** + * 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.utils; + +import com.fasterxml.jackson.core.JsonProcessingException; +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.util.List; + +/** + * @author maissa Souissi + */ +public final class FilterUtils { + + // Utility class, so no constructor + private FilterUtils() { + } + + private static T fromStringToDTO(String jsonString, ObjectMapper objectMapper, TypeReference typeReference, T defaultValue) { + if (StringUtils.isEmpty(jsonString)) { + return defaultValue; + } + try { + return objectMapper.readValue(jsonString, typeReference); + } catch (JsonProcessingException e) { + throw new ComputationException(ComputationException.Type.INVALID_FILTER_FORMAT); + } + } + + public static List fromStringFiltersToDTO(String stringFilters, ObjectMapper objectMapper) { + return fromStringToDTO(stringFilters, objectMapper, new TypeReference<>() { + }, List.of()); + } + + public static GlobalFilter fromStringGlobalFiltersToDTO(String stringGlobalFilters, ObjectMapper objectMapper) { + return fromStringToDTO(stringGlobalFilters, objectMapper, new TypeReference<>() { + }, null); + } +} + diff --git a/src/main/java/org/gridsuite/computation/utils/MessageUtils.java b/src/main/java/org/gridsuite/computation/utils/MessageUtils.java new file mode 100644 index 0000000..6fc5632 --- /dev/null +++ b/src/main/java/org/gridsuite/computation/utils/MessageUtils.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024, 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.utils; + +import com.powsybl.commons.PowsyblException; +import org.apache.commons.lang3.StringUtils; +import org.springframework.lang.Nullable; +import org.springframework.messaging.MessageHeaders; + +/** + * @author Thang PHAM + */ +public final class MessageUtils { + public static final int MSG_MAX_LENGTH = 256; + + private MessageUtils() { + throw new AssertionError("Suppress default constructor for noninstantiability"); + } + + public static String getNonNullHeader(MessageHeaders headers, String name) { + String header = headers.get(name, String.class); + if (header == null) { + throw new PowsyblException("Header '" + name + "' not found"); + } + return header; + } + + /** + * Prevent the message from being too long for RabbitMQ. + * @apiNote the beginning and ending are both kept, it should make it easier to identify + */ + public static String shortenMessage(@Nullable final String msg) { + return StringUtils.abbreviateMiddle(msg, " ... ", MSG_MAX_LENGTH); + } +} diff --git a/src/main/java/org/gridsuite/computation/utils/SpecificationUtils.java b/src/main/java/org/gridsuite/computation/utils/SpecificationUtils.java new file mode 100644 index 0000000..74abf2c --- /dev/null +++ b/src/main/java/org/gridsuite/computation/utils/SpecificationUtils.java @@ -0,0 +1,267 @@ +/** + * 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/. + */ + +/** + * 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.utils; + +import com.google.common.collect.Lists; +import jakarta.persistence.criteria.*; +import org.gridsuite.computation.dto.ResourceFilterDTO; +import org.jetbrains.annotations.NotNull; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.query.EscapeCharacter; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import static org.springframework.data.jpa.domain.Specification.anyOf; +import static org.springframework.data.jpa.domain.Specification.not; + +/** + * Utility class to create Spring Data JPA Specification (Spring interface for JPA Criteria API). + * + * @author Kevin Le Saulnier + */ +public final class SpecificationUtils { + /** + * Maximum values per IN clause chunk to avoid StackOverflow exceptions. + * Current value (500) is a safe default but can be changed + */ + public static final int MAX_IN_CLAUSE_SIZE = 500; + + public static final String FIELD_SEPARATOR = "."; + + // Utility class, so no constructor + private SpecificationUtils() { } + + // we use .as(String.class) to be able to works on enum fields + public static Specification equals(String field, String value) { + return (root, cq, cb) -> cb.equal( + cb + .upper(getColumnPath(root, field).as(String.class)) + .as(String.class), + value.toUpperCase() + ); + } + + public static Specification notEqual(String field, String value) { + return (root, cq, cb) -> cb.notEqual(getColumnPath(root, field), value); + } + + public static Specification contains(String field, String value) { + return (root, cq, cb) -> cb.like(cb.upper(getColumnPath(root, field).as(String.class)), "%" + EscapeCharacter.DEFAULT.escape(value).toUpperCase() + "%", EscapeCharacter.DEFAULT.getEscapeCharacter()); + } + + public static Specification startsWith(String field, String value) { + return (root, cq, cb) -> cb.like(cb.upper(getColumnPath(root, field).as(String.class)), EscapeCharacter.DEFAULT.escape(value).toUpperCase() + "%", EscapeCharacter.DEFAULT.getEscapeCharacter()); + } + + /** + * Returns a specification where the field value is not equal within the given tolerance. + */ + public static Specification notEqual(String field, Double value, Double tolerance) { + return (root, cq, cb) -> { + Expression doubleExpression = getColumnPath(root, field).as(Double.class); + /* + * in order to be equal to doubleExpression, value has to fit : + * value - tolerance <= doubleExpression <= value + tolerance + * therefore in order to be different at least one of the opposite comparison needs to be true : + */ + return cb.or( + cb.greaterThan(doubleExpression, value + tolerance), + cb.lessThan(doubleExpression, value - tolerance) + ); + }; + } + + public static Specification lessThanOrEqual(String field, Double value, Double tolerance) { + return (root, cq, cb) -> { + Expression doubleExpression = getColumnPath(root, field).as(Double.class); + return cb.lessThanOrEqualTo(doubleExpression, value + tolerance); + }; + } + + public static Specification greaterThanOrEqual(String field, Double value, Double tolerance) { + return (root, cq, cb) -> { + Expression doubleExpression = getColumnPath(root, field).as(Double.class); + return cb.greaterThanOrEqualTo(doubleExpression, value - tolerance); + }; + } + + public static Specification isNotEmpty(String field) { + return (root, cq, cb) -> cb.isNotEmpty(getColumnPath(root, field)); + } + + public static Specification distinct() { + return (root, cq, cb) -> { + // to select distinct result, we need to set a "criteria query" param + // we don't need to return any predicate here + cq.distinct(true); + return null; + }; + } + + public static Specification appendFiltersToSpecification(Specification specification, List resourceFilters) { + Objects.requireNonNull(specification); + + if (resourceFilters == null || resourceFilters.isEmpty()) { + return specification; + } + + Specification completedSpecification = specification; + + for (ResourceFilterDTO resourceFilter : resourceFilters) { + if (resourceFilter.dataType() == ResourceFilterDTO.DataType.TEXT) { + completedSpecification = appendTextFilterToSpecification(completedSpecification, resourceFilter); + } else if (resourceFilter.dataType() == ResourceFilterDTO.DataType.NUMBER) { + completedSpecification = appendNumberFilterToSpecification(completedSpecification, resourceFilter); + } + } + + return completedSpecification; + } + + @NotNull + private static Specification appendTextFilterToSpecification(Specification specification, ResourceFilterDTO resourceFilter) { + Specification completedSpecification = specification; + + switch (resourceFilter.type()) { + case NOT_EQUAL, EQUALS, IN -> { + // this type can manage one value or a list of values (with OR) + if (resourceFilter.value() instanceof Collection valueList) { + // implicitly an IN resourceFilter type because only IN may have value lists as filter value + List inValues = valueList.stream() + .map(Object::toString) + .toList(); + completedSpecification = completedSpecification.and( + generateInSpecification(resourceFilter.column(), inValues) + ); + } else if (resourceFilter.value() == null) { + // if the value is null, we build an impossible specification (trick to remove later on ?) + completedSpecification = completedSpecification.and(not(completedSpecification)); + } else { + completedSpecification = completedSpecification.and(equals(resourceFilter.column(), resourceFilter.value().toString())); + } + } + case CONTAINS -> { + if (resourceFilter.value() instanceof Collection valueList) { + completedSpecification = completedSpecification.and( + anyOf( + valueList + .stream() + .map(value -> SpecificationUtils.contains(resourceFilter.column(), value.toString())) + .toList() + )); + } else { + completedSpecification = completedSpecification.and(contains(resourceFilter.column(), resourceFilter.value().toString())); + } + } + case STARTS_WITH -> + completedSpecification = completedSpecification.and(startsWith(resourceFilter.column(), resourceFilter.value().toString())); + default -> throw new IllegalArgumentException("The filter type " + resourceFilter.type() + " is not supported with the data type " + resourceFilter.dataType()); + } + + return completedSpecification; + } + + /** + * Generates a specification for IN clause with the given column and values. + * Handles large value lists by chunking them to avoid StackOverflow. + * + * @param column the column name to filter on + * @param inPossibleValues the list of values for the IN clause + * @return a specification for the IN clause + */ + private static Specification generateInSpecification(String column, List inPossibleValues) { + + if (inPossibleValues.size() > MAX_IN_CLAUSE_SIZE) { + // there are too many values for only one call to anyOf() : it might cause a StackOverflow + // => the specification is divided into several specifications which have an OR between them : + List> chunksOfInValues = Lists.partition(inPossibleValues, MAX_IN_CLAUSE_SIZE); + Specification containerSpec = null; + for (List chunk : chunksOfInValues) { + Specification multiOrEqualSpec = anyOf( + chunk + .stream() + .map(value -> SpecificationUtils.equals(column, value)) + .toList() + ); + if (containerSpec == null) { + containerSpec = multiOrEqualSpec; + } else { + containerSpec = containerSpec.or(multiOrEqualSpec); + } + } + return containerSpec; + } + return anyOf(inPossibleValues + .stream() + .map(value -> SpecificationUtils.equals(column, value)) + .toList() + ); + } + + @NotNull + private static Specification appendNumberFilterToSpecification(Specification specification, ResourceFilterDTO resourceFilter) { + String filterValue = resourceFilter.value().toString(); + double tolerance; + if (resourceFilter.tolerance() != null) { + tolerance = resourceFilter.tolerance(); + } else { + // the reference for the comparison is the number of digits after the decimal point in filterValue + // extra digits are ignored, but the user may add '0's after the decimal point in order to get a better precision + String[] splitValue = filterValue.split("\\."); + int numberOfDecimalAfterDot = 0; + if (splitValue.length > 1) { + numberOfDecimalAfterDot = splitValue[1].length(); + } + // tolerance is multiplied by 0.5 to simulate the fact that the database value is rounded (in the front, from the user viewpoint) + // more than 13 decimal after dot will likely cause rounding errors due to double precision + tolerance = Math.pow(10, -numberOfDecimalAfterDot) * 0.5; + } + Double valueDouble = Double.valueOf(filterValue); + return switch (resourceFilter.type()) { + case NOT_EQUAL -> specification.and(notEqual(resourceFilter.column(), valueDouble, tolerance)); + case LESS_THAN_OR_EQUAL -> + specification.and(lessThanOrEqual(resourceFilter.column(), valueDouble, tolerance)); + case GREATER_THAN_OR_EQUAL -> + specification.and(greaterThanOrEqual(resourceFilter.column(), valueDouble, tolerance)); + default -> + throw new IllegalArgumentException("The filter type " + resourceFilter.type() + " is not supported with the data type " + resourceFilter.dataType()); + }; + } + + /** + * This method allow to query eventually dot separated fields with the Criteria API + * Ex : from 'fortescueCurrent.positiveMagnitude' we create the query path + * root.get("fortescueCurrent").get("positiveMagnitude") to access to the correct nested field + * + * @param root the root entity + * @param dotSeparatedFields dot separated fields (can be only one field without any dot) + * @param the entity type referenced by the root + * @param the type referenced by the path + * @return path for the query + */ + private static Path getColumnPath(Root root, String dotSeparatedFields) { + if (dotSeparatedFields.contains(SpecificationUtils.FIELD_SEPARATOR)) { + String[] fields = dotSeparatedFields.split("\\."); + Path path = root.get(fields[0]); + for (int i = 1; i < fields.length; i++) { + path = path.get(fields[i]); + } + return path; + } else { + return root.get(dotSeparatedFields); + } + } +} diff --git a/src/main/java/org/gridsuite/computation/utils/annotations/PostCompletion.java b/src/main/java/org/gridsuite/computation/utils/annotations/PostCompletion.java new file mode 100644 index 0000000..1230927 --- /dev/null +++ b/src/main/java/org/gridsuite/computation/utils/annotations/PostCompletion.java @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2023, 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.utils.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Anis Touri > RUNNABLE = new ThreadLocal<>(); + + // register a new runnable for post completion execution + public void execute(Runnable runnable) { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + List runnables = RUNNABLE.get(); + if (runnables == null) { + runnables = new LinkedList<>(); + RUNNABLE.set(runnables); + } + runnables.add(runnable); + TransactionSynchronizationManager.registerSynchronization(this); + } else { + // if transaction synchronisation is not active + runnable.run(); + } + } + + @Override + public void afterCompletion(int status) { + List runnables = RUNNABLE.get(); + runnables.forEach(Runnable::run); + RUNNABLE.remove(); + } +} diff --git a/src/main/java/org/gridsuite/computation/utils/annotations/PostCompletionAnnotationAspect.java b/src/main/java/org/gridsuite/computation/utils/annotations/PostCompletionAnnotationAspect.java new file mode 100644 index 0000000..1e82bef --- /dev/null +++ b/src/main/java/org/gridsuite/computation/utils/annotations/PostCompletionAnnotationAspect.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2023, 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.utils.annotations; + +import lombok.AllArgsConstructor; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +/** + * @author Anis Touri { + try { + pjp.proceed(pjp.getArgs()); + } catch (Throwable e) { + throw new PostCompletionException(e); + } + }); + return null; + } +} diff --git a/src/main/java/org/gridsuite/computation/utils/annotations/PostCompletionException.java b/src/main/java/org/gridsuite/computation/utils/annotations/PostCompletionException.java new file mode 100644 index 0000000..0250eab --- /dev/null +++ b/src/main/java/org/gridsuite/computation/utils/annotations/PostCompletionException.java @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2024, 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.utils.annotations; + +/** + * @author Slimane Amar + */ +public class PostCompletionException extends RuntimeException { + public PostCompletionException(Throwable t) { + super(t); + } +} diff --git a/src/test/java/org/gridsuite/computation/ComputationExceptionTest.java b/src/test/java/org/gridsuite/computation/ComputationExceptionTest.java new file mode 100644 index 0000000..fb9b670 --- /dev/null +++ b/src/test/java/org/gridsuite/computation/ComputationExceptionTest.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2024, 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.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * @author Joris Mancini + */ +class ComputationExceptionTest { + + @Test + void testMessageConstructor() { + var e = new ComputationException("test"); + assertEquals("test", e.getMessage()); + } + + @Test + void testMessageAndThrowableConstructor() { + var cause = new RuntimeException("test"); + var e = new ComputationException("test", cause); + assertEquals("test", e.getMessage()); + assertEquals(cause, e.getCause()); + } +} diff --git a/src/test/java/org/gridsuite/computation/ComputationTest.java b/src/test/java/org/gridsuite/computation/ComputationTest.java new file mode 100644 index 0000000..bab316c --- /dev/null +++ b/src/test/java/org/gridsuite/computation/ComputationTest.java @@ -0,0 +1,330 @@ +/** + * 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 com.fasterxml.jackson.databind.ObjectMapper; +import com.powsybl.iidm.network.Network; +import com.powsybl.iidm.network.VariantManager; +import com.powsybl.network.store.client.NetworkStoreService; +import com.powsybl.network.store.client.PreloadingStrategy; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.observation.ObservationRegistry; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.assertj.core.api.WithAssertions; +import org.gridsuite.computation.dto.ReportInfos; +import org.gridsuite.computation.service.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.cloud.stream.function.StreamBridge; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; + +import static org.gridsuite.computation.service.NotificationService.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith({ MockitoExtension.class }) +@Slf4j +class ComputationTest implements WithAssertions { + private static final String COMPUTATION_TYPE = "mockComputation"; + @Mock + private VariantManager variantManager; + @Mock + private NetworkStoreService networkStoreService; + @Mock + private ReportService reportService; + private final ExecutionService executionService = new ExecutionService(); + private final UuidGeneratorService uuidGeneratorService = new UuidGeneratorService(); + @Mock + private StreamBridge publisher; + private NotificationService notificationService; + @Mock + private ObjectMapper objectMapper; + @Mock + private Network network; + + private enum MockComputationStatus { + NOT_DONE, + RUNNING, + COMPLETED + } + + private static class MockComputationResultService extends AbstractComputationResultService { + Map mockDBStatus = new HashMap<>(); + + @Override + public void insertStatus(List resultUuids, MockComputationStatus status) { + resultUuids.forEach(uuid -> + mockDBStatus.put(uuid, status)); + } + + @Override + public void delete(UUID resultUuid) { + mockDBStatus.remove(resultUuid); + } + + @Override + public void deleteAll() { + mockDBStatus.clear(); + } + + @Override + public MockComputationStatus findStatus(UUID resultUuid) { + return mockDBStatus.get(resultUuid); + } + } + + private static class MockComputationObserver extends AbstractComputationObserver { + protected MockComputationObserver(@NonNull ObservationRegistry observationRegistry, @NonNull MeterRegistry meterRegistry) { + super(observationRegistry, meterRegistry); + } + + @Override + protected String getComputationType() { + return COMPUTATION_TYPE; + } + + @Override + protected String getResultStatus(Object res) { + return res != null ? "OK" : "NOK"; + } + } + + private static class MockComputationRunContext extends AbstractComputationRunContext { + // makes the mock computation to behave in a specific way + @Getter @Setter + ComputationResultWanted computationResWanted = ComputationResultWanted.SUCCESS; + + protected MockComputationRunContext(UUID networkUuid, String variantId, String receiver, ReportInfos reportInfos, + String userId, String provider, Object parameters) { + super(networkUuid, variantId, receiver, reportInfos, userId, provider, parameters); + } + } + + private static class MockComputationResultContext extends AbstractResultContext { + protected MockComputationResultContext(UUID resultUuid, MockComputationRunContext runContext) { + super(resultUuid, runContext); + } + } + + private static class MockComputationService extends AbstractComputationService { + protected MockComputationService(NotificationService notificationService, MockComputationResultService resultService, ObjectMapper objectMapper, UuidGeneratorService uuidGeneratorService, String defaultProvider) { + super(notificationService, resultService, objectMapper, uuidGeneratorService, defaultProvider); + } + + @Override + public List getProviders() { + return List.of(); + } + + @Override + public UUID runAndSaveResult(MockComputationRunContext runContext) { + return RESULT_UUID; + } + } + + private enum ComputationResultWanted { + SUCCESS, + FAIL, + CANCELLED, + COMPLETED + } + + private static class MockComputationWorkerService extends AbstractWorkerService { + protected MockComputationWorkerService(NetworkStoreService networkStoreService, NotificationService notificationService, ReportService reportService, MockComputationResultService resultService, ExecutionService executionService, AbstractComputationObserver observer, ObjectMapper objectMapper) { + super(networkStoreService, notificationService, reportService, resultService, executionService, observer, objectMapper); + } + + @Override + protected AbstractResultContext fromMessage(Message message) { + return resultContext; + } + + @Override + protected void saveResult(Network network, AbstractResultContext resultContext, Object result) { } + + @Override + protected String getComputationType() { + return COMPUTATION_TYPE; + } + + @Override + protected CompletableFuture getCompletableFuture(MockComputationRunContext runContext, String provider, UUID resultUuid) { + final CompletableFuture completableFuture = new CompletableFuture<>(); + switch (runContext.getComputationResWanted()) { + case CANCELLED: + completableFuture.completeExceptionally(new CancellationException("Computation cancelled")); + break; + case FAIL: + completableFuture.completeExceptionally(new RuntimeException("Computation failed")); + break; + case SUCCESS: + return CompletableFuture.supplyAsync(Object::new); + case COMPLETED: + return CompletableFuture.completedFuture(null); + } + return completableFuture; + } + + public void addFuture(UUID id, CompletableFuture future) { + this.futures.put(id, future); + } + } + + private MockComputationWorkerService workerService; + private MockComputationService computationService; + private static MockComputationResultContext resultContext; + final UUID networkUuid = UUID.fromString("11111111-1111-1111-1111-111111111111"); + final UUID reportUuid = UUID.fromString("22222222-2222-2222-2222-222222222222"); + static final UUID RESULT_UUID = UUID.fromString("33333333-3333-3333-3333-333333333333"); + final String reporterId = "44444444-4444-4444-4444-444444444444"; + final String userId = "MockComputation_UserId"; + final String receiver = "MockComputation_Receiver"; + final String provider = "MockComputation_Provider"; + Message message; + MockComputationRunContext runContext; + MockComputationResultService resultService; + + @BeforeEach + void init() { + resultService = new MockComputationResultService(); + notificationService = new NotificationService(publisher); + workerService = new MockComputationWorkerService( + networkStoreService, + notificationService, + reportService, + resultService, + executionService, + new MockComputationObserver(ObservationRegistry.create(), new SimpleMeterRegistry()), + objectMapper + ); + computationService = new MockComputationService(notificationService, resultService, objectMapper, uuidGeneratorService, provider); + + MessageBuilder builder = MessageBuilder + .withPayload("") + .setHeader(HEADER_RESULT_UUID, RESULT_UUID.toString()) + .setHeader(HEADER_RECEIVER, receiver) + .setHeader(HEADER_USER_ID, userId); + message = builder.build(); + + runContext = new MockComputationRunContext(networkUuid, null, receiver, + new ReportInfos(reportUuid, reporterId, COMPUTATION_TYPE), userId, provider, new Object()); + resultContext = new MockComputationResultContext(RESULT_UUID, runContext); + } + + private void initComputationExecution() { + when(networkStoreService.getNetwork(eq(networkUuid), any(PreloadingStrategy.class))) + .thenReturn(network); + when(network.getVariantManager()).thenReturn(variantManager); + } + + @Test + void testComputationSuccess() { + // inits + initComputationExecution(); + runContext.setComputationResWanted(ComputationResultWanted.SUCCESS); + + // execution / cleaning + workerService.consumeRun().accept(message); + + // test the course + verify(notificationService.getPublisher(), times(1)).send(eq("publishResult-out-0"), isA(Message.class)); + } + + @Test + void testComputationFailed() { + // inits + initComputationExecution(); + runContext.setComputationResWanted(ComputationResultWanted.FAIL); + + // execution / cleaning + assertThrows(ComputationException.class, () -> workerService.consumeRun().accept(message)); + assertNull(resultService.findStatus(RESULT_UUID)); + } + + @Test + void testStopComputationSendsCancelMessage() { + computationService.stop(RESULT_UUID, receiver); + verify(notificationService.getPublisher(), times(1)).send(eq("publishCancel-out-0"), isA(Message.class)); + } + + @Test + void testComputationCancelledInConsumeRun() { + // inits + initComputationExecution(); + runContext.setComputationResWanted(ComputationResultWanted.CANCELLED); + + // execution / cleaning + workerService.consumeRun().accept(message); + + // test the course + assertNull(resultService.findStatus(RESULT_UUID)); + verify(notificationService.getPublisher(), times(0)).send(eq("publishResult-out-0"), isA(Message.class)); + } + + @Test + void testComputationCancelledInConsumeCancel() { + MockComputationStatus baseStatus = MockComputationStatus.RUNNING; + computationService.setStatus(List.of(RESULT_UUID), baseStatus); + assertEquals(baseStatus, computationService.getStatus(RESULT_UUID)); + + CompletableFuture futureThatCouldBeCancelled = Mockito.mock(CompletableFuture.class); + when(futureThatCouldBeCancelled.cancel(true)).thenReturn(true); + workerService.addFuture(RESULT_UUID, futureThatCouldBeCancelled); + + workerService.consumeCancel().accept(message); + assertNull(resultService.findStatus(RESULT_UUID)); + verify(notificationService.getPublisher(), times(1)).send(eq("publishStopped-out-0"), isA(Message.class)); + } + + @Test + void testComputationCancelFailed() { + MockComputationStatus baseStatus = MockComputationStatus.RUNNING; + computationService.setStatus(List.of(RESULT_UUID), baseStatus); + assertEquals(baseStatus, computationService.getStatus(RESULT_UUID)); + + CompletableFuture futureThatCouldNotBeCancelled = Mockito.mock(CompletableFuture.class); + when(futureThatCouldNotBeCancelled.cancel(true)).thenReturn(false); + workerService.addFuture(RESULT_UUID, futureThatCouldNotBeCancelled); + + workerService.consumeCancel().accept(message); + assertNotNull(resultService.findStatus(RESULT_UUID)); + verify(notificationService.getPublisher(), times(1)).send(eq("publishCancelFailed-out-0"), isA(Message.class)); + } + + @Test + void testComputationCancelFailsIfNoMatchingFuture() { + workerService.consumeCancel().accept(message); + assertNull(resultService.findStatus(RESULT_UUID)); + verify(notificationService.getPublisher(), times(1)).send(eq("publishCancelFailed-out-0"), isA(Message.class)); + } + + @Test + void testComputationCancelledBeforeRunReturnsNoResult() { + workerService.consumeCancel().accept(message); + + initComputationExecution(); + workerService.consumeRun().accept(message); + verify(notificationService.getPublisher(), times(0)).send(eq("publishResult-out-0"), isA(Message.class)); + } +} diff --git a/src/test/java/org/gridsuite/computation/ComputationUtilTest.java b/src/test/java/org/gridsuite/computation/ComputationUtilTest.java new file mode 100644 index 0000000..bae3391 --- /dev/null +++ b/src/test/java/org/gridsuite/computation/ComputationUtilTest.java @@ -0,0 +1,179 @@ +/** + * Copyright (c) 2024, 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 com.powsybl.iidm.network.*; +import com.powsybl.network.store.iidm.impl.NetworkFactoryImpl; +import com.powsybl.security.*; +import org.gridsuite.computation.utils.ComputationResultUtils; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Jamal KHEYYAD + */ +class ComputationUtilTest { + + static Network createBusBreakerNetwork() { + Network network = new NetworkFactoryImpl().createNetwork("network", "test"); + Substation p1 = network.newSubstation() + .setId("P1") + .setCountry(Country.FR) + .setTso("RTE") + .setGeographicalTags("A") + .add(); + VoltageLevel vl = p1.newVoltageLevel() + .setId("VLGEN") + .setNominalV(24.0) + .setTopologyKind(TopologyKind.BUS_BREAKER) + .add(); + + Bus ngen = vl.getBusBreakerView().newBus() + .setId("NGEN") + .add(); + + Bus ngen2 = vl.getBusBreakerView().newBus() + .setId("NGEN2") + .add(); + + vl.newLoad() + .setId("LD") + .setBus(ngen.getId()) + .setConnectableBus(ngen.getId()) + .setP0(600.0) + .setQ0(200.0) + .add(); + + vl.newLoad() + .setId("LD2") + .setBus(ngen2.getId()) + .setConnectableBus(ngen2.getId()) + .setP0(600.0) + .setQ0(200.0) + .add(); + + return network; + } + + public static Network createNodeBreakerNetwork() { + Network network = Network.create("network", "test"); + Substation s = network.newSubstation() + .setId("S") + .add(); + VoltageLevel vl = s.newVoltageLevel() + .setId("VL1") + .setNominalV(400) + .setLowVoltageLimit(370.) + .setHighVoltageLimit(420.) + .setTopologyKind(TopologyKind.NODE_BREAKER) + .add(); + vl.getNodeBreakerView().newBusbarSection() + .setId("BBS1") + .setNode(0) + .add(); + vl.getNodeBreakerView().newBusbarSection() + .setId("BBS2") + .setNode(1) + .add(); + + vl.newLoad() + .setId("LD") + .setNode(3) + .setP0(600.0) + .setQ0(200.0) + .add(); + + vl.getNodeBreakerView().newBreaker() + .setId("BR") + .setOpen(false) + .setNode1(0) + .setNode2(3) + .add(); + + return network; + } + + @Test + void testViolationLocationIdBusBreaker() { + Network network = createBusBreakerNetwork(); + + LimitViolation limitViolation = mock(LimitViolation.class); + + when(limitViolation.getLimitType()).thenReturn(LimitViolationType.HIGH_VOLTAGE); + when(limitViolation.getViolationLocation()).thenReturn(Optional.of(new BusBreakerViolationLocation(List.of("NGEN")))); + assertEquals("VLGEN_0", ComputationResultUtils.getViolationLocationId(limitViolation, network)); + + when(limitViolation.getViolationLocation()).thenReturn(Optional.of(new BusBreakerViolationLocation(List.of("NGEN2")))); + assertEquals("VLGEN_1", ComputationResultUtils.getViolationLocationId(limitViolation, network)); + + when(limitViolation.getViolationLocation()).thenReturn(Optional.of(new BusBreakerViolationLocation(List.of("NGEN", "NGEN2")))); + when(limitViolation.getSubjectId()).thenReturn("VLGEN"); + assertEquals("VLGEN (VLGEN_0, VLGEN_1)", ComputationResultUtils.getViolationLocationId(limitViolation, network)); + + when(limitViolation.getViolationLocation()).thenReturn(Optional.of(new BusBreakerViolationLocation(List.of()))); + when(limitViolation.getSubjectId()).thenReturn("VLGEN"); + assertEquals("VLGEN", ComputationResultUtils.getViolationLocationId(limitViolation, network)); + } + + @Test + void testNoViolationLocationIdNodeBreaker() { + Network network = createNodeBreakerNetwork(); + LimitViolation limitViolation = mock(LimitViolation.class); + + when(limitViolation.getLimitType()).thenReturn(LimitViolationType.CURRENT); + when(limitViolation.getViolationLocation()).thenReturn(Optional.empty()); + when(limitViolation.getSubjectId()).thenReturn("subjectId"); + assertEquals(null, ComputationResultUtils.getViolationLocationId(limitViolation, network)); + } + + @Test + void testNoViolationLocationIdBusBreaker() { + Network network = createBusBreakerNetwork(); + LimitViolation limitViolation = mock(LimitViolation.class); + + when(limitViolation.getLimitType()).thenReturn(LimitViolationType.HIGH_VOLTAGE); + when(limitViolation.getViolationLocation()).thenReturn(Optional.empty()); + when(limitViolation.getSubjectId()).thenReturn("subjectId"); + assertEquals("subjectId", ComputationResultUtils.getViolationLocationId(limitViolation, network)); + } + + @Test + void testViolationLocationIdNodeBreaker() { + Network network = createNodeBreakerNetwork(); + + NodeBreakerViolationLocation nodeBreakerViolationLocation = mock(NodeBreakerViolationLocation.class); + when(nodeBreakerViolationLocation.getType()).thenReturn(ViolationLocation.Type.NODE_BREAKER); + when(nodeBreakerViolationLocation.getVoltageLevelId()).thenReturn("VL1"); + when(nodeBreakerViolationLocation.getNodes()).thenReturn(List.of()); + + LimitViolation limitViolation = mock(LimitViolation.class); + when(limitViolation.getLimitType()).thenReturn(LimitViolationType.HIGH_VOLTAGE); + when(limitViolation.getViolationLocation()).thenReturn(Optional.of(nodeBreakerViolationLocation)); + when(limitViolation.getSubjectId()).thenReturn("VLHV1"); + + String locationId = ComputationResultUtils.getViolationLocationId(limitViolation, network); + assertEquals("VL1", locationId); + + when(nodeBreakerViolationLocation.getNodes()).thenReturn(List.of(0, 1)); + locationId = ComputationResultUtils.getViolationLocationId(limitViolation, network); + assertEquals("VL1 (BBS1, BBS2)", locationId); + + when(nodeBreakerViolationLocation.getNodes()).thenReturn(List.of(0)); + locationId = ComputationResultUtils.getViolationLocationId(limitViolation, network); + assertEquals("VL1_0", locationId); + + when(nodeBreakerViolationLocation.getNodes()).thenReturn(List.of(1)); + locationId = ComputationResultUtils.getViolationLocationId(limitViolation, network); + assertEquals("VL1 (BBS2)", locationId); + } +} diff --git a/src/test/java/org/gridsuite/computation/service/FilterServiceTest.java b/src/test/java/org/gridsuite/computation/service/FilterServiceTest.java new file mode 100644 index 0000000..6615e17 --- /dev/null +++ b/src/test/java/org/gridsuite/computation/service/FilterServiceTest.java @@ -0,0 +1,1146 @@ +/** + * 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.service; + +import com.powsybl.commons.PowsyblException; +import com.powsybl.iidm.network.Country; +import com.powsybl.iidm.network.Network; +import com.powsybl.iidm.network.TwoSides; +import com.powsybl.iidm.network.VariantManager; +import com.powsybl.network.store.client.NetworkStoreService; +import com.powsybl.network.store.client.PreloadingStrategy; +import org.gridsuite.computation.dto.GlobalFilter; +import org.gridsuite.computation.dto.ResourceFilterDTO; +import org.gridsuite.filter.AbstractFilter; +import org.gridsuite.filter.expertfilter.ExpertFilter; +import org.gridsuite.filter.expertfilter.expertrule.*; +import org.gridsuite.filter.identifierlistfilter.IdentifiableAttributes; +import org.gridsuite.filter.utils.EquipmentType; +import org.gridsuite.filter.utils.FilterServiceUtils; +import org.gridsuite.filter.utils.expertfilter.CombinatorType; +import org.gridsuite.filter.utils.expertfilter.FieldType; +import org.gridsuite.filter.utils.expertfilter.OperatorType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.server.ResponseStatusException; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.*; + +/** + * @author Rehili Ghazwa + */ +@ExtendWith(MockitoExtension.class) +class FilterServiceTest { + + @Mock + private NetworkStoreService networkStoreService; + + @Mock + private RestTemplate restTemplate; + + @Mock + private Network network; + + @Mock + private VariantManager variantManager; + + @Mock + private AbstractFilterService filterService; + + private static final String FILTER_SERVER_BASE_URI = "http://localhost:8080"; + private static final String VARIANT_ID = "testVariant"; + private static final UUID NETWORK_UUID = UUID.randomUUID(); + private static final UUID FILTER_UUID = UUID.randomUUID(); + + @Test + void shouldReturnEmptyListWhenFiltersUuidsIsEmpty() { + // Given + when(filterService.getFilters(anyList())).thenCallRealMethod(); + List emptyList = Collections.emptyList(); + + // When + List result = filterService.getFilters(emptyList); + + // Then + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + void shouldReturnEmptyListWhenFiltersUuidsIsNull() { + // Given + when(filterService.getFilters(any())).thenCallRealMethod(); + + // When + List result = filterService.getFilters(null); + + // Then + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + void houldCallRestTemplateAndReturnFilters() { + // Given + when(filterService.getFilters(anyList())).thenCallRealMethod(); + ReflectionTestUtils.setField(filterService, "restTemplate", restTemplate); + ReflectionTestUtils.setField(filterService, "filterServerBaseUri", FILTER_SERVER_BASE_URI); + + List filterUuids = List.of(FILTER_UUID); + List expectedFilters = Collections.singletonList(mock(AbstractFilter.class)); + + ResponseEntity> responseEntity = new ResponseEntity<>(expectedFilters, HttpStatus.OK); + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), isNull(), any(ParameterizedTypeReference.class))).thenReturn(responseEntity); + + // When + List result = filterService.getFilters(filterUuids); + + // Then + assertEquals(expectedFilters, result); + verify(restTemplate).exchange( + contains("v1/filters/metadata"), + eq(HttpMethod.GET), + isNull(), + any(ParameterizedTypeReference.class) + ); + } + + @Test + void shouldThrowPowsyblExceptionWhenHttpError() { + // Given + when(filterService.getFilters(anyList())).thenCallRealMethod(); + ReflectionTestUtils.setField(filterService, "restTemplate", restTemplate); + ReflectionTestUtils.setField(filterService, "filterServerBaseUri", FILTER_SERVER_BASE_URI); + + List filterUuids = List.of(FILTER_UUID); + + HttpStatusCodeException httpException = mock(HttpStatusCodeException.class); + when(restTemplate.exchange( + anyString(), + eq(HttpMethod.GET), + isNull(), + any(ParameterizedTypeReference.class) + )).thenThrow(httpException); + + // When & Then + PowsyblException exception = assertThrows(PowsyblException.class, + () -> filterService.getFilters(filterUuids)); + + assertTrue(exception.getMessage().contains("Filters not found")); + assertTrue(exception.getMessage().contains(FILTER_UUID.toString())); + } + + @Test + void shouldReturnNetworkWhenSuccessful() { + // Given + when(filterService.getNetwork(any(), any())).thenCallRealMethod(); + ReflectionTestUtils.setField(filterService, "networkStoreService", networkStoreService); + + when(networkStoreService.getNetwork(NETWORK_UUID, PreloadingStrategy.COLLECTION)) + .thenReturn(network); + when(network.getVariantManager()).thenReturn(variantManager); + + // When + Network result = filterService.getNetwork(NETWORK_UUID, VARIANT_ID); + + // Then + assertEquals(network, result); + verify(variantManager).setWorkingVariant(VARIANT_ID); + } + + @Test + void shouldThrowResponseStatusExceptionWhenPowsyblException() { + // Given + when(filterService.getNetwork(any(), any())).thenCallRealMethod(); + ReflectionTestUtils.setField(filterService, "networkStoreService", networkStoreService); + + PowsyblException powsyblException = new PowsyblException("Network not found"); + when(networkStoreService.getNetwork(NETWORK_UUID, PreloadingStrategy.COLLECTION)) + .thenThrow(powsyblException); + + // When & Then + ResponseStatusException exception = assertThrows(ResponseStatusException.class, + () -> filterService.getNetwork(NETWORK_UUID, VARIANT_ID)); + + assertEquals(HttpStatus.NOT_FOUND, exception.getStatusCode()); + assertEquals("Network not found", exception.getReason()); + } + + @Test + void shouldReturnEmptyListWhenNumberExpertRulesValuesIsNull() { + // Given + when(filterService.createNumberExpertRules(any(), any())).thenCallRealMethod(); + + // When + List result = filterService.createNumberExpertRules(null, FieldType.NOMINAL_VOLTAGE); + + // Then + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + void shouldCreateNumberExpertRulesWhenValuesProvided() { + // Given + when(filterService.createNumberExpertRules(any(), any())).thenCallRealMethod(); + List values = Arrays.asList("400.0", "225.0"); + + // When + List result = filterService.createNumberExpertRules(values, FieldType.NOMINAL_VOLTAGE); + + // Then + assertNotNull(result); + assertEquals(2, result.size()); + + for (AbstractExpertRule rule : result) { + assertInstanceOf(NumberExpertRule.class, rule); + NumberExpertRule numberRule = (NumberExpertRule) rule; + assertEquals(FieldType.NOMINAL_VOLTAGE, numberRule.getField()); + assertEquals(OperatorType.EQUALS, numberRule.getOperator()); + } + } + + @Test + void shouldCreateCorrectPropertiesRule() { + // Given + when(filterService.createPropertiesRule(any(), any(), any())).thenCallRealMethod(); + String property = "testProperty"; + List values = Arrays.asList("value1", "value2"); + + // When + AbstractExpertRule result = filterService.createPropertiesRule(property, values, FieldType.SUBSTATION_PROPERTIES); + + // Then + assertNotNull(result); + assertInstanceOf(PropertiesExpertRule.class, result); + + PropertiesExpertRule propertiesRule = (PropertiesExpertRule) result; + assertEquals(CombinatorType.OR, propertiesRule.getCombinator()); + assertEquals(OperatorType.IN, propertiesRule.getOperator()); + assertEquals(FieldType.SUBSTATION_PROPERTIES, propertiesRule.getField()); + assertEquals(property, propertiesRule.getPropertyName()); + assertEquals(values, propertiesRule.getPropertyValues()); + } + + @Test + void shouldReturnEmptyListWhenEnumExpertRulesValuesIsNull() { + // Given + when(filterService.createEnumExpertRules(any(), any())).thenCallRealMethod(); + + // When + List result = filterService.createEnumExpertRules(null, FieldType.COUNTRY); + + // Then + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + void shouldCreateRulesWhenValuesProvided() { + // Given + when(filterService.createEnumExpertRules(any(), any())).thenCallRealMethod(); + List values = Arrays.asList(Country.FR, Country.DE); + + // When + List result = filterService.createEnumExpertRules(values, FieldType.COUNTRY); + + // Then + assertNotNull(result); + assertEquals(2, result.size()); + + for (AbstractExpertRule rule : result) { + assertInstanceOf(EnumExpertRule.class, rule); + EnumExpertRule enumRule = (EnumExpertRule) rule; + assertEquals(FieldType.COUNTRY, enumRule.getField()); + assertEquals(OperatorType.EQUALS, enumRule.getOperator()); + } + } + + @Test + void shouldCreateCombinatorRule() { + // Given + when(filterService.createCombination(any(), any())).thenCallRealMethod(); + List rules = Collections.singletonList(mock(AbstractExpertRule.class)); + + // When + AbstractExpertRule result = filterService.createCombination(CombinatorType.AND, rules); + + // Then + assertNotNull(result); + assertInstanceOf(CombinatorExpertRule.class, result); + + CombinatorExpertRule combinatorRule = (CombinatorExpertRule) result; + assertEquals(CombinatorType.AND, combinatorRule.getCombinator()); + assertEquals(rules, combinatorRule.getRules()); + } + + @Test + void shouldReturnEmptyWhenRulesIsEmpty() { + // Given + when(filterService.createOrCombination(any())).thenCallRealMethod(); + List rules = Collections.emptyList(); + + // When + Optional result = filterService.createOrCombination(rules); + + // Then + assertFalse(result.isPresent()); + } + + @Test + void shouldReturnSingleRuleWhenOnlyOneRule() { + // Given + when(filterService.createOrCombination(any())).thenCallRealMethod(); + + AbstractExpertRule rule = mock(AbstractExpertRule.class); + List rules = Collections.singletonList(rule); + + // When + Optional result = filterService.createOrCombination(rules); + + // Then + assertTrue(result.isPresent()); + assertEquals(rule, result.get()); + } + + @Test + void shouldReturnCombinatorWhenMultipleRules() { + // Given + when(filterService.createOrCombination(any())).thenCallRealMethod(); + when(filterService.createCombination(any(), any())).thenCallRealMethod(); + + List rules = Arrays.asList( + mock(AbstractExpertRule.class), + mock(AbstractExpertRule.class) + ); + + // When + Optional result = filterService.createOrCombination(rules); + + // Then + assertTrue(result.isPresent()); + assertInstanceOf(CombinatorExpertRule.class, result.get()); + + CombinatorExpertRule combinatorRule = (CombinatorExpertRule) result.get(); + assertEquals(CombinatorType.OR, combinatorRule.getCombinator()); + } + + @Test + void shouldReturnEmptyWhenCombineFilterResultsInputIsEmpty() { + // Given + when(filterService.combineFilterResults(any(), anyBoolean())).thenCallRealMethod(); + List> filterResults = Collections.emptyList(); + + // When + List result = filterService.combineFilterResults(filterResults, true); + + // Then + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + void shouldReturnIntersectionWhenUsingAndLogic() { + // Given + when(filterService.combineFilterResults(any(), anyBoolean())).thenCallRealMethod(); + List> filterResults = Arrays.asList( + Arrays.asList("item1", "item2", "item3"), + Arrays.asList("item2", "item3", "item4"), + Arrays.asList("item2", "item5") + ); + + // When + List result = filterService.combineFilterResults(filterResults, true); + + // Then + assertEquals(1, result.size()); + assertTrue(result.contains("item2")); + } + + @Test + void shouldReturnUnionWhenUsingOrLogic() { + // Given + when(filterService.combineFilterResults(any(), anyBoolean())).thenCallRealMethod(); + List> filterResults = Arrays.asList( + Arrays.asList("item1", "item2"), + Arrays.asList("item3", "item4"), + List.of("item5") + ); + + // When + List result = filterService.combineFilterResults(filterResults, false); + + // Then + assertEquals(5, result.size()); + assertTrue(result.containsAll(Arrays.asList("item1", "item2", "item3", "item4", "item5"))); + } + + @Test + void shouldReturnIdsFromFilteredNetwork() { + // Given + when(filterService.filterNetwork(any(), any())).thenCallRealMethod(); + AbstractFilter filter = mock(AbstractFilter.class); + + try (MockedStatic mockStatic = mockStatic(FilterServiceUtils.class)) { + List attributes = Arrays.asList( + createIdentifiableAttributes("id1"), + createIdentifiableAttributes("id2") + ); + + mockStatic.when(() -> FilterServiceUtils.getIdentifiableAttributes(filter, network, filterService)) + .thenReturn(attributes); + + // When + List result = filterService.filterNetwork(filter, network); + + // Then + assertEquals(Arrays.asList("id1", "id2"), result); + } + } + + @Test + void shouldCreateCorrectRuleForSideOne() { + // Given + when(filterService.createVoltageLevelIdRule(any(), any())).thenCallRealMethod(); + UUID filterUuid = UUID.randomUUID(); + + // When + AbstractExpertRule result = filterService.createVoltageLevelIdRule(filterUuid, TwoSides.ONE); + + // Then + assertNotNull(result); + assertInstanceOf(FilterUuidExpertRule.class, result); + + FilterUuidExpertRule rule = (FilterUuidExpertRule) result; + assertEquals(OperatorType.IS_PART_OF, rule.getOperator()); + assertEquals(FieldType.VOLTAGE_LEVEL_ID_1, rule.getField()); + assertTrue(rule.getValues().contains(filterUuid.toString())); + } + + @Test + void shouldCreateCorrectRuleForSideTwo() { + // Given + when(filterService.createVoltageLevelIdRule(any(), any())).thenCallRealMethod(); + UUID filterUuid = UUID.randomUUID(); + + // When + AbstractExpertRule result = filterService.createVoltageLevelIdRule(filterUuid, TwoSides.TWO); + + // Then + assertNotNull(result); + assertInstanceOf(FilterUuidExpertRule.class, result); + + FilterUuidExpertRule rule = (FilterUuidExpertRule) result; + assertEquals(OperatorType.IS_PART_OF, rule.getOperator()); + assertEquals(FieldType.VOLTAGE_LEVEL_ID_2, rule.getField()); + assertTrue(rule.getValues().contains(filterUuid.toString())); + } + + @Test + void shouldReturnEmptyWhenNoExpertFiltersProvided() { + // Given + when(filterService.buildAllExpertRules(any(), any())).thenCallRealMethod(); + when(filterService.buildNominalVoltageRules(any(), any())).thenCallRealMethod(); + + GlobalFilter globalFilter = mock(GlobalFilter.class); + when(globalFilter.getNominalV()).thenReturn(null); + when(globalFilter.getCountryCode()).thenReturn(null); + when(globalFilter.getSubstationProperty()).thenReturn(null); + + // When + List result = filterService.buildAllExpertRules(globalFilter, EquipmentType.GENERATOR); + + // Then + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + void shouldReturnRulesWhenFilterExpertRulesProvided() { + // Given + when(filterService.buildAllExpertRules(any(), any())).thenCallRealMethod(); + when(filterService.buildNominalVoltageRules(any(), any())).thenCallRealMethod(); + when(filterService.buildCountryCodeRules(any(), any())).thenCallRealMethod(); + when(filterService.buildSubstationPropertyRules(any(), any())).thenCallRealMethod(); + when(filterService.getNominalVoltageFieldType(any())).thenReturn(List.of(FieldType.NOMINAL_VOLTAGE)); + when(filterService.getCountryCodeFieldType(any())).thenReturn(List.of(FieldType.COUNTRY)); + when(filterService.getSubstationPropertiesFieldTypes(any())).thenReturn(List.of(FieldType.SUBSTATION_PROPERTIES)); + when(filterService.createNumberExpertRules(any(), any())).thenCallRealMethod(); + when(filterService.createEnumExpertRules(any(), any())).thenCallRealMethod(); + when(filterService.createPropertiesRule(any(), any(), any())).thenCallRealMethod(); + when(filterService.createOrCombination(any())).thenCallRealMethod(); + + GlobalFilter globalFilter = mock(GlobalFilter.class); + when(globalFilter.getNominalV()).thenReturn(List.of("400.0")); + when(globalFilter.getCountryCode()).thenReturn(List.of(Country.FR)); + when(globalFilter.getSubstationProperty()).thenReturn(Map.of("prop1", List.of("value1"))); + + // When + List result = filterService.buildAllExpertRules(globalFilter, EquipmentType.GENERATOR); + + // Then + assertNotNull(result); + assertEquals(3, result.size()); + } + + @Test + void shouldReturnEmptyWhenNoNominalVoltageRules() { + // Given + when(filterService.buildNominalVoltageRules(any(), any())).thenCallRealMethod(); + when(filterService.getNominalVoltageFieldType(any())).thenReturn(List.of(FieldType.NOMINAL_VOLTAGE)); + + // When + Optional result = filterService.buildNominalVoltageRules(null, EquipmentType.GENERATOR); + + // Then + assertFalse(result.isPresent()); + } + + @Test + void shouldCreateRulesWhenNominalVoltageRulesProvided() { + // Given + when(filterService.buildNominalVoltageRules(any(), any())).thenCallRealMethod(); + when(filterService.getNominalVoltageFieldType(any())).thenReturn(List.of(FieldType.NOMINAL_VOLTAGE)); + when(filterService.createNumberExpertRules(any(), any())).thenCallRealMethod(); + when(filterService.createOrCombination(any())).thenCallRealMethod(); + when(filterService.createCombination(any(), any())).thenCallRealMethod(); + + List voltages = Arrays.asList("400.0", "225.0"); + + // When + Optional result = filterService.buildNominalVoltageRules(voltages, EquipmentType.GENERATOR); + + // Then + assertTrue(result.isPresent()); + } + + @Test + void shouldReturnEmptyWhenNoCountriesCodeRules() { + // Given + when(filterService.buildCountryCodeRules(any(), any())).thenCallRealMethod(); + when(filterService.getCountryCodeFieldType(any())).thenReturn(List.of(FieldType.COUNTRY)); + + // When + Optional result = filterService.buildCountryCodeRules(null, EquipmentType.GENERATOR); + + // Then + assertFalse(result.isPresent()); + } + + @Test + void shouldCreateRulesWhenCountriesCountryCodeRulesProvided() { + // Given + when(filterService.buildCountryCodeRules(any(), any())).thenCallRealMethod(); + when(filterService.getCountryCodeFieldType(any())).thenReturn(List.of(FieldType.COUNTRY)); + when(filterService.createEnumExpertRules(any(), any())).thenCallRealMethod(); + when(filterService.createOrCombination(any())).thenCallRealMethod(); + when(filterService.createCombination(any(), any())).thenCallRealMethod(); + + List countries = Arrays.asList(Country.FR, Country.DE); + + // When + Optional result = filterService.buildCountryCodeRules(countries, EquipmentType.GENERATOR); + + // Then + assertTrue(result.isPresent()); + } + + @Test + void shouldCreateRulesWhenSubstationPropertiesProvided() { + // Given + when(filterService.buildSubstationPropertyRules(any(), any())).thenCallRealMethod(); + when(filterService.getSubstationPropertiesFieldTypes(any())).thenReturn(List.of(FieldType.SUBSTATION_PROPERTIES)); + when(filterService.createPropertiesRule(any(), any(), any())).thenCallRealMethod(); + when(filterService.createOrCombination(any())).thenCallRealMethod(); + when(filterService.createCombination(any(), any())).thenCallRealMethod(); + + Map> properties = Map.of( + "prop1", Arrays.asList("value1", "value2"), + "prop2", List.of("value3") + ); + + // When + Optional result = filterService.buildSubstationPropertyRules(properties, EquipmentType.GENERATOR); + + // Then + assertTrue(result.isPresent()); + } + + @Test + void shouldReturnNullWhenNoRules() { + // Given + when(filterService.buildExpertFilter(any(), any())).thenCallRealMethod(); + when(filterService.buildAllExpertRules(any(), any())).thenCallRealMethod(); + when(filterService.buildNominalVoltageRules(any(), any())).thenCallRealMethod(); + + GlobalFilter globalFilter = mock(GlobalFilter.class); + when(globalFilter.getNominalV()).thenReturn(null); + when(globalFilter.getCountryCode()).thenReturn(null); + when(globalFilter.getSubstationProperty()).thenReturn(null); + + // When + ExpertFilter result = filterService.buildExpertFilter(globalFilter, EquipmentType.GENERATOR); + + // Then + assertNull(result); + } + + @Test + void shouldCreateFilterWhenRulesExist() { + // Given + when(filterService.buildExpertFilter(any(), any())).thenCallRealMethod(); + when(filterService.buildAllExpertRules(any(), any())).thenCallRealMethod(); + when(filterService.buildNominalVoltageRules(any(), any())).thenCallRealMethod(); + when(filterService.createCombination(any(), any())).thenCallRealMethod(); + when(filterService.getNominalVoltageFieldType(any())).thenReturn(List.of(FieldType.NOMINAL_VOLTAGE)); + when(filterService.createNumberExpertRules(any(), any())).thenCallRealMethod(); + when(filterService.createOrCombination(any())).thenCallRealMethod(); + + GlobalFilter globalFilter = mock(GlobalFilter.class); + when(globalFilter.getNominalV()).thenReturn(List.of("400.0")); + when(globalFilter.getCountryCode()).thenReturn(null); + when(globalFilter.getSubstationProperty()).thenReturn(null); + + // When + ExpertFilter result = filterService.buildExpertFilter(globalFilter, EquipmentType.GENERATOR); + + // Then + assertNotNull(result); + assertEquals(EquipmentType.GENERATOR, result.getEquipmentType()); + assertNotNull(result.getRules()); + assertInstanceOf(CombinatorExpertRule.class, result.getRules()); + } + + @Test + void shouldReturnFilteredNetworkWhenSameEquipmentType() { + // Given + when(filterService.extractEquipmentIdsFromGenericFilter(any(), any(), any())).thenCallRealMethod(); + when(filterService.filterNetwork(any(), any())).thenCallRealMethod(); + + AbstractFilter filter = mock(AbstractFilter.class); + when(filter.getEquipmentType()).thenReturn(EquipmentType.GENERATOR); + + try (MockedStatic mockStatic = mockStatic(FilterServiceUtils.class)) { + List attributes = Arrays.asList( + createIdentifiableAttributes("gen1"), + createIdentifiableAttributes("gen2") + ); + mockStatic.when(() -> FilterServiceUtils.getIdentifiableAttributes(filter, network, filterService)) + .thenReturn(attributes); + + // When + List result = filterService.extractEquipmentIdsFromGenericFilter( + filter, EquipmentType.GENERATOR, network); + + // Then + assertEquals(Arrays.asList("gen1", "gen2"), result); + } + } + + @Test + void shouldBuildVoltageLevelFilterWhenVoltageLevelType() { + // Given + when(filterService.extractEquipmentIdsFromGenericFilter(any(), any(), any())).thenCallRealMethod(); + when(filterService.buildExpertFilterWithVoltageLevelIdsCriteria(any(), any())).thenCallRealMethod(); + when(filterService.createVoltageLevelIdRule(any(), any())).thenCallRealMethod(); + when(filterService.createCombination(any(), any())).thenCallRealMethod(); + when(filterService.filterNetwork(any(), any())).thenCallRealMethod(); + + AbstractFilter filter = mock(AbstractFilter.class); + when(filter.getEquipmentType()).thenReturn(EquipmentType.VOLTAGE_LEVEL); + when(filter.getId()).thenReturn(FILTER_UUID); + + try (MockedStatic mockStatic = mockStatic(FilterServiceUtils.class)) { + List attributes = Arrays.asList( + createIdentifiableAttributes("line1"), + createIdentifiableAttributes("line2") + ); + mockStatic.when(() -> FilterServiceUtils.getIdentifiableAttributes(any(ExpertFilter.class), eq(network), eq(filterService))) + .thenReturn(attributes); + + // When + List result = filterService.extractEquipmentIdsFromGenericFilter( + filter, EquipmentType.LINE, network); + + // Then + assertEquals(Arrays.asList("line1", "line2"), result); + } + } + + @Test + void shouldReturnEmptyWhenDifferentEquipmentType() { + // Given + when(filterService.extractEquipmentIdsFromGenericFilter(any(), any(), any())).thenCallRealMethod(); + + AbstractFilter filter = mock(AbstractFilter.class); + when(filter.getEquipmentType()).thenReturn(EquipmentType.LOAD); + + // When + List result = filterService.extractEquipmentIdsFromGenericFilter( + filter, EquipmentType.GENERATOR, network); + + // Then + assertTrue(result.isEmpty()); + } + + @Test + void shouldCreateExpertFilterWithVoltageLevelIdsCriteria() { + // Given + when(filterService.buildExpertFilterWithVoltageLevelIdsCriteria(any(), any())).thenCallRealMethod(); + when(filterService.createVoltageLevelIdRule(any(), any())).thenCallRealMethod(); + when(filterService.createCombination(any(), any())).thenCallRealMethod(); + + UUID filterUuid = UUID.randomUUID(); + EquipmentType equipmentType = EquipmentType.LINE; + + // When + ExpertFilter result = filterService.buildExpertFilterWithVoltageLevelIdsCriteria(filterUuid, equipmentType); + + // Then + assertNotNull(result); + assertEquals(equipmentType, result.getEquipmentType()); + assertNotNull(result.getRules()); + assertInstanceOf(CombinatorExpertRule.class, result.getRules()); + + CombinatorExpertRule combinatorRule = (CombinatorExpertRule) result.getRules(); + assertEquals(CombinatorType.OR, combinatorRule.getCombinator()); + assertEquals(2, combinatorRule.getRules().size()); + } + + @Test + void testGetNominalVoltageFieldType() { + // Given + when(filterService.getNominalVoltageFieldType(any())).thenCallRealMethod(); + + // When + List result = filterService.getNominalVoltageFieldType(EquipmentType.LINE); + + // Then + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals(FieldType.NOMINAL_VOLTAGE_1, result.get(0)); + assertEquals(FieldType.NOMINAL_VOLTAGE_2, result.get(1)); + } + + @Test + void testGetCountryCodeField() { + // Given + when(filterService.getCountryCodeFieldType(any())).thenCallRealMethod(); + + // When + List result = filterService.getCountryCodeFieldType(EquipmentType.LINE); + + // Then + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals(FieldType.COUNTRY_1, result.get(0)); + assertEquals(FieldType.COUNTRY_2, result.get(1)); + } + + @Test + void testGetSubstationPropertiesFieldTypes() { + // Given + when(filterService.getSubstationPropertiesFieldTypes(any())).thenCallRealMethod(); + + // When + List result = filterService.getSubstationPropertiesFieldTypes(EquipmentType.LINE); + + // Then + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals(FieldType.SUBSTATION_PROPERTIES_1, result.get(0)); + assertEquals(FieldType.SUBSTATION_PROPERTIES_2, result.get(1)); + } + + @Test + void testGetSubstationPropertiesFieldTypesWithNonLineEquipmentTypes() { + // Given + when(filterService.getSubstationPropertiesFieldTypes(any())).thenCallRealMethod(); + + // Test various non-LINE equipment types that should return single SUBSTATION_PROPERTIES + EquipmentType[] nonLineTypes = { + EquipmentType.VOLTAGE_LEVEL, EquipmentType.TWO_WINDINGS_TRANSFORMER, EquipmentType.GENERATOR, + EquipmentType.LOAD, EquipmentType.SHUNT_COMPENSATOR, EquipmentType.STATIC_VAR_COMPENSATOR, + EquipmentType.BATTERY, EquipmentType.BUSBAR_SECTION, EquipmentType.LCC_CONVERTER_STATION, + EquipmentType.VSC_CONVERTER_STATION, EquipmentType.SUBSTATION, EquipmentType.THREE_WINDINGS_TRANSFORMER + }; + + for (EquipmentType equipmentType : nonLineTypes) { + // When + List result = filterService.getSubstationPropertiesFieldTypes(equipmentType); + + // Then + assertNotNull(result, "Result should not be null for " + equipmentType); + assertEquals(1, result.size(), "Result should have size 1 for " + equipmentType); + assertEquals(FieldType.SUBSTATION_PROPERTIES, result.getFirst(), "Should return SUBSTATION_PROPERTIES for " + equipmentType); + assertTrue(result.contains(FieldType.SUBSTATION_PROPERTIES), "Should contain SUBSTATION_PROPERTIES for " + equipmentType); + } + } + + // Tests for filterEquipmentsByType + @Test + void testFilterEquipmentsByTypeWithEmptyEquipmentTypes() { + // Given + when(filterService.filterEquipmentsByType(any(), any(), any(), any())).thenCallRealMethod(); + GlobalFilter globalFilter = mock(GlobalFilter.class); + List genericFilters = Collections.emptyList(); + List equipmentTypes = Collections.emptyList(); + + // When + Map> result = filterService.filterEquipmentsByType( + network, globalFilter, genericFilters, equipmentTypes); + + // Then + assertNotNull(result); + assertTrue(result.isEmpty()); + assertInstanceOf(EnumMap.class, result); + } + + @Test + void testFilterEquipmentsByTypeWithEquipmentTypes() { + // Given + when(filterService.filterEquipmentsByType(any(), any(), any(), any())).thenCallRealMethod(); + when(filterService.extractFilteredEquipmentIds(any(), any(), any(), any())).thenReturn(Arrays.asList("line1", "line2")); + + GlobalFilter globalFilter = mock(GlobalFilter.class); + List genericFilters = Collections.emptyList(); + List equipmentTypes = Arrays.asList(EquipmentType.LINE, EquipmentType.GENERATOR); + + // When + Map> result = filterService.filterEquipmentsByType( + network, globalFilter, genericFilters, equipmentTypes); + + // Then + assertNotNull(result); + assertInstanceOf(EnumMap.class, result); + assertEquals(2, result.size()); + assertTrue(result.containsKey(EquipmentType.LINE)); + assertTrue(result.containsKey(EquipmentType.GENERATOR)); + assertEquals(Arrays.asList("line1", "line2"), result.get(EquipmentType.LINE)); + assertEquals(Arrays.asList("line1", "line2"), result.get(EquipmentType.GENERATOR)); + } + + @Test + void testFilterEquipmentsByTypeWithEmptyFilterResults() { + // Given + when(filterService.filterEquipmentsByType(any(), any(), any(), any())).thenCallRealMethod(); + when(filterService.extractFilteredEquipmentIds(any(), any(), any(), any())).thenReturn(Collections.emptyList()); + + GlobalFilter globalFilter = mock(GlobalFilter.class); + List genericFilters = Collections.emptyList(); + List equipmentTypes = List.of(EquipmentType.LINE); + + // When + Map> result = filterService.filterEquipmentsByType( + network, globalFilter, genericFilters, equipmentTypes); + + // Then + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + void testFilterEquipmentsByTypeWithMixedResults() { + // Given + when(filterService.filterEquipmentsByType(any(), any(), any(), any())).thenCallRealMethod(); + when(filterService.extractFilteredEquipmentIds(any(), any(), any(), eq(EquipmentType.LINE))) + .thenReturn(Arrays.asList("line1", "line2")); + when(filterService.extractFilteredEquipmentIds(any(), any(), any(), eq(EquipmentType.GENERATOR))) + .thenReturn(Collections.emptyList()); + when(filterService.extractFilteredEquipmentIds(any(), any(), any(), eq(EquipmentType.LOAD))) + .thenReturn(List.of("load1")); + + GlobalFilter globalFilter = mock(GlobalFilter.class); + List genericFilters = Collections.emptyList(); + List equipmentTypes = Arrays.asList(EquipmentType.LINE, EquipmentType.GENERATOR, EquipmentType.LOAD); + + // When + Map> result = filterService.filterEquipmentsByType( + network, globalFilter, genericFilters, equipmentTypes); + + // Then + assertNotNull(result); + assertEquals(2, result.size()); // Only LINE and LOAD should be present (not GENERATOR with empty results) + assertTrue(result.containsKey(EquipmentType.LINE)); + assertTrue(result.containsKey(EquipmentType.LOAD)); + assertFalse(result.containsKey(EquipmentType.GENERATOR)); + assertEquals(Arrays.asList("line1", "line2"), result.get(EquipmentType.LINE)); + assertEquals(List.of("load1"), result.get(EquipmentType.LOAD)); + } + + // Tests for extractFilteredEquipmentIds + @Test + void testExtractFilteredEquipmentIdsWithEmptyFilters() { + // Given + when(filterService.extractFilteredEquipmentIds(any(), any(), any(), any())).thenCallRealMethod(); + when(filterService.buildExpertFilter(any(), any())).thenReturn(null); + when(filterService.combineFilterResults(any(), anyBoolean())).thenCallRealMethod(); + + GlobalFilter globalFilter = mock(GlobalFilter.class); + List genericFilters = Collections.emptyList(); + + // When + List result = filterService.extractFilteredEquipmentIds( + network, globalFilter, genericFilters, EquipmentType.LINE); + + // Then + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + void testExtractFilteredEquipmentIdsWithExpertFilter() { + // Given + when(filterService.extractFilteredEquipmentIds(any(), any(), any(), any())).thenCallRealMethod(); + when(filterService.buildExpertFilter(any(), any())).thenReturn(mock(ExpertFilter.class)); + when(filterService.filterNetwork(any(), any())).thenReturn(Arrays.asList("line1", "line2")); + when(filterService.combineFilterResults(any(), anyBoolean())).thenCallRealMethod(); + + GlobalFilter globalFilter = mock(GlobalFilter.class); + List genericFilters = Collections.emptyList(); + + // When + List result = filterService.extractFilteredEquipmentIds( + network, globalFilter, genericFilters, EquipmentType.LINE); + + // Then + assertNotNull(result); + assertEquals(Arrays.asList("line1", "line2"), result); + } + + @Test + void testExtractFilteredEquipmentIdsWithGenericFilters() { + // Given + when(filterService.extractFilteredEquipmentIds(any(), any(), any(), any())).thenCallRealMethod(); + when(filterService.buildExpertFilter(any(), any())).thenReturn(null); + when(filterService.extractEquipmentIdsFromGenericFilter(any(), any(), any())) + .thenReturn(Arrays.asList("line3", "line4")); + when(filterService.combineFilterResults(any(), anyBoolean())).thenCallRealMethod(); + + GlobalFilter globalFilter = mock(GlobalFilter.class); + AbstractFilter genericFilter = mock(AbstractFilter.class); + List genericFilters = Collections.singletonList(genericFilter); + + // When + List result = filterService.extractFilteredEquipmentIds( + network, globalFilter, genericFilters, EquipmentType.LINE); + + // Then + assertNotNull(result); + assertEquals(Arrays.asList("line3", "line4"), result); + } + + @Test + void testExtractFilteredEquipmentIdsWithBothFilters() { + // Given + when(filterService.extractFilteredEquipmentIds(any(), any(), any(), any())).thenCallRealMethod(); + when(filterService.buildExpertFilter(any(), any())).thenReturn(mock(ExpertFilter.class)); + when(filterService.filterNetwork(any(), any())).thenReturn(Arrays.asList("line1", "line2")); + when(filterService.extractEquipmentIdsFromGenericFilter(any(), any(), any())) + .thenReturn(Arrays.asList("line2", "line3")); + when(filterService.combineFilterResults(any(), anyBoolean())).thenCallRealMethod(); + + GlobalFilter globalFilter = mock(GlobalFilter.class); + AbstractFilter genericFilter = mock(AbstractFilter.class); + List genericFilters = Collections.singletonList(genericFilter); + + // When + List result = filterService.extractFilteredEquipmentIds( + network, globalFilter, genericFilters, EquipmentType.LINE); + + // Then + assertNotNull(result); + assertEquals(List.of("line2"), result); // Intersection of both filter results + } + + @Test + void testExtractFilteredEquipmentIdsWithMultipleGenericFilters() { + AbstractFilter genericFilter1 = mock(AbstractFilter.class); + AbstractFilter genericFilter2 = mock(AbstractFilter.class); + GlobalFilter globalFilter = mock(GlobalFilter.class); + // Given + when(filterService.extractFilteredEquipmentIds(any(), any(), any(), any())).thenCallRealMethod(); + when(filterService.buildExpertFilter(any(), any())).thenReturn(null); + when(filterService.extractEquipmentIdsFromGenericFilter(eq(genericFilter1), any(), any())) + .thenReturn(Arrays.asList("line1", "line2")); + when(filterService.extractEquipmentIdsFromGenericFilter(eq(genericFilter2), any(), any())) + .thenReturn(Arrays.asList("line2", "line3")); + when(filterService.combineFilterResults(any(), anyBoolean())).thenCallRealMethod(); + + List genericFilters = Arrays.asList(genericFilter1, genericFilter2); + + // When + List result = filterService.extractFilteredEquipmentIds( + network, globalFilter, genericFilters, EquipmentType.LINE); + + // Then + assertNotNull(result); + assertEquals(List.of("line2"), result); // AND logic for generic filters + } + + // Tests for getResourceFilter + @Test + void testGetResourceFilterWithEmptyResults() { + // Given + when(filterService.getResourceFilter(any(), any(), any(), any(), any())).thenCallRealMethod(); + when(filterService.getNetwork(any(), any())).thenReturn(network); + when(filterService.getFilters(any())).thenReturn(Collections.emptyList()); + when(filterService.filterEquipmentsByType(any(), any(), any(), any())) + .thenReturn(Collections.emptyMap()); + + GlobalFilter globalFilter = mock(GlobalFilter.class); + when(globalFilter.getGenericFilter()).thenReturn(Collections.emptyList()); + List equipmentTypes = List.of(EquipmentType.LINE); + String columnName = "functionId"; + + // When + Optional result = filterService.getResourceFilter(NETWORK_UUID, VARIANT_ID, globalFilter, equipmentTypes, columnName); + + // Then + assertNotNull(result); + assertFalse(result.isPresent()); + } + + @Test + void testGetResourceFilterWithResults() { + // Given + when(filterService.getResourceFilter(any(), any(), any(), any(), any())).thenCallRealMethod(); + when(filterService.getNetwork(any(), any())).thenReturn(network); + when(filterService.getFilters(any())).thenReturn(Collections.emptyList()); + + Map> equipmentResults = new EnumMap<>(EquipmentType.class); + equipmentResults.put(EquipmentType.LINE, Arrays.asList("line1", "line2")); + equipmentResults.put(EquipmentType.GENERATOR, List.of("gen1")); + when(filterService.filterEquipmentsByType(any(), any(), any(), any())).thenReturn(equipmentResults); + + GlobalFilter globalFilter = mock(GlobalFilter.class); + when(globalFilter.getGenericFilter()).thenReturn(Collections.emptyList()); + List equipmentTypes = Arrays.asList(EquipmentType.LINE, EquipmentType.GENERATOR); + String columnName = "functionId"; + + // When + Optional result = filterService.getResourceFilter( + NETWORK_UUID, VARIANT_ID, globalFilter, equipmentTypes, columnName); + + // Then + assertNotNull(result); + assertTrue(result.isPresent()); + + ResourceFilterDTO dto = result.get(); + assertEquals(ResourceFilterDTO.DataType.TEXT, dto.dataType()); + assertEquals(ResourceFilterDTO.Type.IN, dto.type()); + assertEquals(columnName, dto.column()); + assertEquals(dto.value(), List.of("line1", "line2", "gen1")); + } + + @Test + void testGetResourceFilterWithNullValues() { + // Given + when(filterService.getResourceFilter(any(), any(), any(), any(), any())).thenCallRealMethod(); + when(filterService.getNetwork(any(), any())).thenReturn(network); + when(filterService.getFilters(any())).thenReturn(Collections.emptyList()); + + Map> equipmentResults = new EnumMap<>(EquipmentType.class); + equipmentResults.put(EquipmentType.LINE, Arrays.asList("line1", "line2")); + equipmentResults.put(EquipmentType.GENERATOR, null); // null value should be filtered out + when(filterService.filterEquipmentsByType(any(), any(), any(), any())).thenReturn(equipmentResults); + + GlobalFilter globalFilter = mock(GlobalFilter.class); + when(globalFilter.getGenericFilter()).thenReturn(Collections.emptyList()); + List equipmentTypes = Arrays.asList(EquipmentType.LINE, EquipmentType.GENERATOR); + String columnName = "functionId"; + + // When + Optional result = filterService.getResourceFilter( + NETWORK_UUID, VARIANT_ID, globalFilter, equipmentTypes, columnName); + + // Then + assertNotNull(result); + assertTrue(result.isPresent()); + + ResourceFilterDTO dto = result.get(); + assertEquals(dto.value(), List.of("line1", "line2")); // Only line1, line2 (null values filtered out) + } + + @Test + void testGetResourceFilterWithGenericFilters() { + // Given + when(filterService.getResourceFilter(any(), any(), any(), any(), any())).thenCallRealMethod(); + when(filterService.getNetwork(any(), any())).thenReturn(network); + + AbstractFilter genericFilter = mock(AbstractFilter.class); + List genericFilters = Collections.singletonList(genericFilter); + when(filterService.getFilters(any())).thenReturn(genericFilters); + + Map> equipmentResults = new EnumMap<>(EquipmentType.class); + equipmentResults.put(EquipmentType.LINE, List.of("line1")); + when(filterService.filterEquipmentsByType(any(), any(), any(), any())).thenReturn(equipmentResults); + + GlobalFilter globalFilter = mock(GlobalFilter.class); + List filterUuids = List.of(UUID.randomUUID()); + when(globalFilter.getGenericFilter()).thenReturn(filterUuids); + List equipmentTypes = List.of(EquipmentType.LINE); + String columnName = "functionId"; + + // When + Optional result = filterService.getResourceFilter( + NETWORK_UUID, VARIANT_ID, globalFilter, equipmentTypes, columnName); + + // Then + assertNotNull(result); + assertTrue(result.isPresent()); + verify(filterService).getFilters(filterUuids); + } + + @Test + void testGetResourceFilterCustomColumnName() { + // Given + when(filterService.getResourceFilter(any(), any(), any(), any(), any())).thenCallRealMethod(); + when(filterService.getNetwork(any(), any())).thenReturn(network); + when(filterService.getFilters(any())).thenReturn(Collections.emptyList()); + + Map> equipmentResults = new EnumMap<>(EquipmentType.class); + equipmentResults.put(EquipmentType.LINE, List.of("line1")); + when(filterService.filterEquipmentsByType(any(), any(), any(), any())).thenReturn(equipmentResults); + + GlobalFilter globalFilter = mock(GlobalFilter.class); + when(globalFilter.getGenericFilter()).thenReturn(Collections.emptyList()); + List equipmentTypes = List.of(EquipmentType.LINE); + String customColumnName = "customColumn"; + + // When + Optional result = filterService.getResourceFilter( + NETWORK_UUID, VARIANT_ID, globalFilter, equipmentTypes, customColumnName); + + // Then + assertNotNull(result); + assertTrue(result.isPresent()); + assertEquals(customColumnName, result.get().column()); + } + + private IdentifiableAttributes createIdentifiableAttributes(String id) { + IdentifiableAttributes attributes = mock(IdentifiableAttributes.class); + when(attributes.getId()).thenReturn(id); + return attributes; + } +} diff --git a/src/test/java/org/gridsuite/computation/service/ReportServiceTest.java b/src/test/java/org/gridsuite/computation/service/ReportServiceTest.java new file mode 100644 index 0000000..0f26c94 --- /dev/null +++ b/src/test/java/org/gridsuite/computation/service/ReportServiceTest.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2024, 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.service; + +import com.powsybl.commons.report.ReportNode; +import org.gridsuite.computation.ComputationConfig; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient; +import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.test.web.client.match.MockRestRequestMatchers; +import org.springframework.test.web.client.response.MockRestResponseCreators; +import org.springframework.web.client.RestClientException; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.*; + +/** + * @author Mathieu Deharbe reportService.sendReport(REPORT_UUID, reportNode)); + } + + @Test + void testSendReportFailed() { + final ReportNode reportNode = ReportNode.newRootReportNode() + .withResourceBundles("i18n.reports") + .withMessageTemplate("test") + .build(); + server.expect(MockRestRequestMatchers.method(HttpMethod.PUT)) + .andExpect(MockRestRequestMatchers.requestTo("http://report-server/v1/reports/" + REPORT_ERROR_UUID)) + .andRespond(MockRestResponseCreators.withServerError()); + assertThatThrownBy(() -> reportService.sendReport(REPORT_ERROR_UUID, reportNode)).isInstanceOf(RestClientException.class); + } + + @Test + void testDeleteReport() { + server.expect(MockRestRequestMatchers.method(HttpMethod.DELETE)) + .andExpect(MockRestRequestMatchers.requestTo("http://report-server/v1/reports/" + REPORT_UUID + "?errorOnReportNotFound=false")) + .andExpect(MockRestRequestMatchers.content().bytes(new byte[0])) + .andRespond(MockRestResponseCreators.withSuccess()); + assertThatNoException().isThrownBy(() -> reportService.deleteReport(REPORT_UUID)); + } + + @Test + void testDeleteReportFailed() { + server.expect(MockRestRequestMatchers.method(HttpMethod.DELETE)) + .andExpect(MockRestRequestMatchers.requestTo("http://report-server/v1/reports/" + REPORT_ERROR_UUID + "?errorOnReportNotFound=false")) + .andExpect(MockRestRequestMatchers.content().bytes(new byte[0])) + .andRespond(MockRestResponseCreators.withServerError()); + assertThatThrownBy(() -> reportService.deleteReport(REPORT_ERROR_UUID)).isInstanceOf(RestClientException.class); + } +} + diff --git a/src/test/java/org/gridsuite/computation/specification/CommonSpecificationBuilderTest.java b/src/test/java/org/gridsuite/computation/specification/CommonSpecificationBuilderTest.java new file mode 100644 index 0000000..d603b2c --- /dev/null +++ b/src/test/java/org/gridsuite/computation/specification/CommonSpecificationBuilderTest.java @@ -0,0 +1,164 @@ +/** + * 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.specification; + +import jakarta.persistence.criteria.*; +import org.gridsuite.computation.dto.ResourceFilterDTO; +import org.gridsuite.computation.utils.SpecificationUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.data.jpa.domain.Specification; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.gridsuite.computation.dto.ResourceFilterDTO.DataType.NUMBER; +import static org.gridsuite.computation.dto.ResourceFilterDTO.DataType.TEXT; +import static org.gridsuite.computation.dto.ResourceFilterDTO.Type.*; +import static org.gridsuite.computation.utils.SpecificationUtils.MAX_IN_CLAUSE_SIZE; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +class CommonSpecificationBuilderTest { + + UUID resUuid = UUID.randomUUID(); + CommonSpecificationBuilderTestImpl builder; + CriteriaBuilder cb; + Root root; + Path path; + Expression exprString; + Expression exprDouble; + + @BeforeEach + void init() { + builder = new CommonSpecificationBuilderTestImpl(); + cb = Mockito.mock(CriteriaBuilder.class); + root = Mockito.mock(Root.class); + path = Mockito.mock(Path.class); + exprString = Mockito.mock(Expression.class); + + // Configure mocks' behavior + when(root.get(anyString())).thenReturn(path); + when(cb.equal(any(Path.class), any(UUID.class))).thenReturn(Mockito.mock(Predicate.class)); + when(path.get(anyString())).thenReturn(path); + when(path.as(String.class)).thenReturn(exprString); + when(path.as(Double.class)).thenReturn(exprDouble); + when(cb.upper(exprString)).thenReturn(exprString); + when(exprString.as(String.class)).thenReturn(exprString); + } + + @Test + void testResultUuidEquals() { + when(root.get("resultId")).thenReturn(path); + when(cb.equal(path, resUuid)).thenReturn(Mockito.mock(Predicate.class)); + + var specification = builder.resultUuidEquals(resUuid); + assertNotNull(specification); + } + + @Test + void testBuildSpecification() { + CriteriaQuery cq = Mockito.mock(CriteriaQuery.class); + + List tooManyInValues = new ArrayList<>( + List.of("dummyColumnValue", "otherDummyColumnValue", "willTriggerChunksSecurity") + ); + for (int i = 0; i < MAX_IN_CLAUSE_SIZE + 1; ++i) { + tooManyInValues.add("dummyValue" + i); + } + + // test data + List resourceFilters = List.of( + new ResourceFilterDTO(TEXT, EQUALS, "dummyColumnValue", "dummyColumn"), + new ResourceFilterDTO(TEXT, STARTS_WITH, "dum", "dummyColumn"), + new ResourceFilterDTO(TEXT, IN, List.of("dummyColumnValue", "otherDummyColumnValue"), "dummyColumn"), + new ResourceFilterDTO(TEXT, IN, tooManyInValues, "dummyColumn"), + new ResourceFilterDTO(TEXT, CONTAINS, "partialValue", "dummyColumn"), + new ResourceFilterDTO(TEXT, CONTAINS, List.of("partialValue1", "partialValue2"), "dummyColumn"), + new ResourceFilterDTO(NUMBER, LESS_THAN_OR_EQUAL, 100.0157, "dummyNumberColumn"), + new ResourceFilterDTO(NUMBER, GREATER_THAN_OR_EQUAL, 10, "dummyNumberColumn", 0.1), + new ResourceFilterDTO(NUMBER, NOT_EQUAL, 10, "parent.dummyNumberColumn") + ); + List resourceFiltersWithChildren = List.of( + new ResourceFilterDTO(NUMBER, NOT_EQUAL, 10, "parent.dummyNumberColumn") + ); + List emptyResourceFilters = List.of(); + + var specification = builder.buildSpecification(resUuid, resourceFilters); + assertNotNull(specification); + Predicate predicate = specification.toPredicate(root, cq, cb); + assertNotNull(predicate); + + var specificationWithChildren = builder.buildSpecification(resUuid, resourceFiltersWithChildren, false); + assertNotNull(specificationWithChildren); + Predicate predicateWithChildren = specificationWithChildren.toPredicate(root, cq, cb); + assertNotNull(predicateWithChildren); + + var emptySpec = builder.buildSpecification(resUuid, emptyResourceFilters, false); + assertNotNull(emptySpec); + Predicate emptyPred = emptySpec.toPredicate(root, cq, cb); + assertNotNull(emptyPred); + } + + @Test + void testBuildLimitViolationsSpecification() { + List resourceFilters = List.of( + new ResourceFilterDTO(NUMBER, NOT_EQUAL, 10, "dummyNumberColumn") + ); + + var specification = builder.buildLimitViolationsSpecification(List.of(resUuid), resourceFilters); + assertNotNull(specification); + } + + @Test + void testInvalidResourceFilters() { + UUID resultUuid = UUID.randomUUID(); + List textResourceFilters = List.of( + new ResourceFilterDTO(TEXT, GREATER_THAN_OR_EQUAL, "dummyValue", "dummyColumn") + ); + assertThrows(IllegalArgumentException.class, () -> builder.buildSpecification(resultUuid, textResourceFilters)); + + List numResourceFilters = List.of( + new ResourceFilterDTO(NUMBER, IN, 1, "dummyColumn") + ); + assertThrows(IllegalArgumentException.class, () -> builder.buildSpecification(resultUuid, numResourceFilters)); + } + + // test specific dummy implementation + private static class CommonSpecificationBuilderTestImpl extends AbstractCommonSpecificationBuilder { + + @Override + public boolean isNotParentFilter(ResourceFilterDTO filter) { + return !filter.column().equals("parent.dummyNumberColumn"); + } + + @Override + public String getIdFieldName() { + return "id"; + } + + @Override + public Path getResultIdPath(Root root) { + return root.get("resultId"); + } + + @Override + public Specification addSpecificFilterWhenNoChildrenFilter() { + return SpecificationUtils.isNotEmpty("dummyColumn"); + } + + @Override + public Specification addSpecificFilterWhenChildrenFilters() { + return addSpecificFilterWhenNoChildrenFilter(); + } + } +} diff --git a/src/test/resources/i18n/reports.properties b/src/test/resources/i18n/reports.properties new file mode 100644 index 0000000..2d4b08b --- /dev/null +++ b/src/test/resources/i18n/reports.properties @@ -0,0 +1 @@ +test = a test From 7e946ce0a37a61dd4bbe52c93ed9f421220ae2f8 Mon Sep 17 00:00:00 2001 From: Rehili Ghazwa Date: Tue, 15 Jul 2025 17:15:11 +0200 Subject: [PATCH 02/10] add dependencies version --- pom.xml | 51 +++++---------------------------------------------- 1 file changed, 5 insertions(+), 46 deletions(-) diff --git a/pom.xml b/pom.xml index 3da4194..9577b23 100644 --- a/pom.xml +++ b/pom.xml @@ -44,30 +44,15 @@ 3.3.3 2023.0.1 - 6.1.12 - 1.18.34 - 2.15.2 - 3.0.2 - 3.1.0 + 2025.0.2 + 1.27.2 + 1.18.34 4.4 - - 5.10.2 2.2 - 5.11.0 - 3.24.2 - 1.5.1 - - 2025.0.1 - 1.27.2 - - 1.6.0-SNAPSHOT - 1.9.22.1 - 1.13.3 - 4.1.1 - 3.3.3 + 1.4.0 gridsuite org.gridsuite:computation @@ -130,22 +115,10 @@ org.junit.jupiter junit-jupiter-params - ${junit.jupiter.version} org.hamcrest hamcrest - ${org.hamcrest.version} - - - org.mockito - mockito-core - ${org.mockito.version} - - - org.assertj - assertj-core - ${assertj.version} @@ -170,50 +143,38 @@ jakarta.validation jakarta.validation-api - ${jakarta.validation.version} jakarta.persistence jakarta.persistence-api - ${jakarta.persistence.version} org.springframework spring-core - ${spring.version} - - - org.springframework - spring-context - ${spring.version} org.springframework spring-messaging - ${spring.version} org.springframework.data spring-data-jpa - ${spring-data-jpa.version} org.springframework.cloud spring-cloud-stream - ${spring-cloud-stream.version} org.aspectj aspectjweaver - ${aspectj.version} true @@ -221,7 +182,6 @@ io.micrometer micrometer-core - ${micrometer.version} true @@ -275,7 +235,6 @@ org.mockito mockito-junit-jupiter - ${org.mockito.version} test @@ -290,6 +249,7 @@ org.hamcrest hamcrest + ${org.hamcrest.version} test @@ -297,7 +257,6 @@ org.skyscreamer jsonassert - ${jsonassert.version} test From c30dd59481c54d9d2d05d8119b9bee87a26cdd48 Mon Sep 17 00:00:00 2001 From: Rehili Ghazwa Date: Wed, 16 Jul 2025 14:58:25 +0200 Subject: [PATCH 03/10] some fix --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 9577b23..d81f98f 100644 --- a/pom.xml +++ b/pom.xml @@ -34,8 +34,8 @@ - Rehili Ghazoua - ghazoua.rehili_externe@rte-france.com + Rehili Ghazwa + ghazwa.rehili_externe@rte-france.com RTE http://www.rte-france.com From 55a7807fc840682c562506d702170ec981137432 Mon Sep 17 00:00:00 2001 From: Rehili Ghazwa Date: Wed, 16 Jul 2025 15:34:28 +0200 Subject: [PATCH 04/10] CI fix --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d81f98f..6716ce6 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,7 @@ - Rehili Ghazwa + Rhili Ghazwa ghazwa.rehili_externe@rte-france.com RTE http://www.rte-france.com From 52b65be416bde454258c8df9673c4b35ea5dfc39 Mon Sep 17 00:00:00 2001 From: Rehili Ghazwa Date: Wed, 16 Jul 2025 15:49:51 +0200 Subject: [PATCH 05/10] sonar failure --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6716ce6..d81f98f 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,7 @@ - Rhili Ghazwa + Rehili Ghazwa ghazwa.rehili_externe@rte-france.com RTE http://www.rte-france.com From b47e501b7b8823c486047550f3e42dab56cd5ebd Mon Sep 17 00:00:00 2001 From: Rehili Ghazwa Date: Thu, 17 Jul 2025 10:52:10 +0200 Subject: [PATCH 06/10] fix README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d955463..7fb7c78 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # computation -[![Actions Status](https://github.com/gridsuite/computation/workflows/CI/badge.svg)](https://github.com/gridsuite/computation/actions) -[![Coverage Status](https://sonarcloud.io/api/project_badges/measure?project=org.gridsuite%computation&metric=coverage)](https://sonarcloud.io/component_measures?id=org.gridsuite%computation&metric=coverage) +[![Actions Status](https://github.com/gridsuite/computation/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/gridsuite/computation/actions) +[![Coverage Status](https://sonarcloud.io/api/project_badges/measure?project=org.gridsuite%3Acomputation&metric=coverage)](https://sonarcloud.io/component_measures?id=org.gridsuite%3Acomputation&metric=coverage) [![MPL-2.0 License](https://img.shields.io/badge/license-MPL_2.0-blue.svg)](https://www.mozilla.org/en-US/MPL/2.0/) -Shared library for common computation and filter classes. \ No newline at end of file +Library for common computation and filter classes. \ No newline at end of file From ddc6b37a8098362ed68c14f7952c4d0a1370fb15 Mon Sep 17 00:00:00 2001 From: Rehili Ghazwa Date: Thu, 17 Jul 2025 11:46:15 +0200 Subject: [PATCH 07/10] code review Thang --- README.md | 2 +- pom.xml | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7fb7c78..4338331 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,4 @@ [![Coverage Status](https://sonarcloud.io/api/project_badges/measure?project=org.gridsuite%3Acomputation&metric=coverage)](https://sonarcloud.io/component_measures?id=org.gridsuite%3Acomputation&metric=coverage) [![MPL-2.0 License](https://img.shields.io/badge/license-MPL_2.0-blue.svg)](https://www.mozilla.org/en-US/MPL/2.0/) -Library for common computation and filter classes. \ No newline at end of file +Common library for computation and result filtering. \ No newline at end of file diff --git a/pom.xml b/pom.xml index d81f98f..fcda7f6 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ jar Computation library - A shared library for common computation and filter classes + Common library for common computation and result filtering. http://www.gridsuite.org/ @@ -42,18 +42,17 @@ + 2.22.0 + 1.28.0 + 1.5.0 + 3.3.3 2023.0.1 - 2025.0.2 - 1.27.2 - 1.18.34 4.4 2.2 - 1.4.0 - gridsuite org.gridsuite:computation @@ -80,8 +79,8 @@ com.powsybl - powsybl-dependencies - ${powsybl-dependencies.version} + powsybl-ws-dependencies + ${powsybl-ws-dependencies.version} pom import From 0ede4d1e76ef56270fcda34fd1e6788ad36b3bd7 Mon Sep 17 00:00:00 2001 From: Rehili Ghazwa Date: Fri, 18 Jul 2025 13:07:26 +0200 Subject: [PATCH 08/10] clean pom file --- pom.xml | 42 ------------------------------------------ 1 file changed, 42 deletions(-) diff --git a/pom.xml b/pom.xml index fcda7f6..b14c9f0 100644 --- a/pom.xml +++ b/pom.xml @@ -43,13 +43,8 @@ 2.22.0 - 1.28.0 1.5.0 - 3.3.3 - 2023.0.1 - - 1.18.34 4.4 2.2 @@ -84,41 +79,6 @@ pom import - - org.springframework.boot - spring-boot-dependencies - ${spring-boot.version} - pom - import - - - org.springframework.cloud - spring-cloud-dependencies - ${spring-cloud.version} - pom - import - true - - - - - org.apache.commons - commons-collections4 - ${org-apache-commons.version} - - - org.projectlombok - lombok - ${lombok.version} - - - org.junit.jupiter - junit-jupiter-params - - - org.hamcrest - hamcrest - @@ -188,7 +148,6 @@ com.powsybl powsybl-network-store-client - ${powsybl-network-store-client.version} true @@ -263,7 +222,6 @@ org.springframework.boot spring-boot-test-autoconfigure - ${spring-boot.version} test From 13ab75e24c0895ed9b44d00dd53b2589b49e160c Mon Sep 17 00:00:00 2001 From: Rehili Ghazwa Date: Fri, 18 Jul 2025 13:34:30 +0200 Subject: [PATCH 09/10] fix sonar issues --- .../computation/service/AbstractResultContext.java | 1 + .../java/org/gridsuite/computation/ComputationTest.java | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/gridsuite/computation/service/AbstractResultContext.java b/src/main/java/org/gridsuite/computation/service/AbstractResultContext.java index 3068a89..b80728d 100644 --- a/src/main/java/org/gridsuite/computation/service/AbstractResultContext.java +++ b/src/main/java/org/gridsuite/computation/service/AbstractResultContext.java @@ -71,6 +71,7 @@ public Message toMessage(ObjectMapper objectMapper) { .build(); } + @SuppressWarnings("unused") protected Map getSpecificMsgHeaders(ObjectMapper ignoredObjectMapper) { return Map.of(); } diff --git a/src/test/java/org/gridsuite/computation/ComputationTest.java b/src/test/java/org/gridsuite/computation/ComputationTest.java index bab316c..5572102 100644 --- a/src/test/java/org/gridsuite/computation/ComputationTest.java +++ b/src/test/java/org/gridsuite/computation/ComputationTest.java @@ -37,6 +37,7 @@ import java.util.UUID; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; import static org.gridsuite.computation.service.NotificationService.*; import static org.junit.jupiter.api.Assertions.*; @@ -161,7 +162,10 @@ protected AbstractResultContext fromMessage(Message resultContext, Object result) { } + protected void saveResult(Network network, AbstractResultContext resultContext, Object result) { + // Empty implementation - this is a mock/test implementation that doesn't need to persist results + // The actual result saving is handled by the real implementation or is not needed for testing + } @Override protected String getComputationType() { @@ -258,7 +262,8 @@ void testComputationFailed() { runContext.setComputationResWanted(ComputationResultWanted.FAIL); // execution / cleaning - assertThrows(ComputationException.class, () -> workerService.consumeRun().accept(message)); + Consumer> consumer = workerService.consumeRun(); + assertThrows(ComputationException.class, () -> consumer.accept(message)); assertNull(resultService.findStatus(RESULT_UUID)); } From e2dc317347aaf40b208d87301290fd93f15d650a Mon Sep 17 00:00:00 2001 From: Rehili Ghazwa Date: Fri, 18 Jul 2025 16:41:03 +0200 Subject: [PATCH 10/10] update dependencies version --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index b14c9f0..3cf2d35 100644 --- a/pom.xml +++ b/pom.xml @@ -42,8 +42,8 @@ - 2.22.0 - 1.5.0 + 2.23.0 + 1.6.0 4.4 2.2