From bc6339a7b3f69acfc753507fc7f22d5fb523c50e Mon Sep 17 00:00:00 2001 From: Franck LECUYER Date: Fri, 20 Feb 2026 14:36:37 +0100 Subject: [PATCH 1/2] POC : monitor-core using servers Signed-off-by: Franck LECUYER --- pom.xml | 8 + .../NetworkModificationOnCaseController.java | 48 +++++ .../server/service/CaseResultInfos.java | 38 ++++ .../service/NetworkConversionService.java | 52 ++++++ .../NetworkModificationOnCaseService.java | 168 ++++++++++++++++++ .../server/service/NotificationService.java | 2 +- src/main/resources/application-local.yml | 2 + src/main/resources/config/application.yaml | 4 +- .../modification/server/reports.properties | 1 + 9 files changed, 321 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/gridsuite/modification/server/NetworkModificationOnCaseController.java create mode 100644 src/main/java/org/gridsuite/modification/server/service/CaseResultInfos.java create mode 100644 src/main/java/org/gridsuite/modification/server/service/NetworkConversionService.java create mode 100644 src/main/java/org/gridsuite/modification/server/service/NetworkModificationOnCaseService.java diff --git a/pom.xml b/pom.xml index 73f08f064..9abb87f15 100644 --- a/pom.xml +++ b/pom.xml @@ -162,6 +162,10 @@ com.powsybl powsybl-iidm-api + + com.powsybl + powsybl-iidm-impl + com.powsybl powsybl-iidm-modification @@ -199,6 +203,10 @@ org.springframework.boot spring-boot-starter-actuator + + com.powsybl + powsybl-case-datasource-client + diff --git a/src/main/java/org/gridsuite/modification/server/NetworkModificationOnCaseController.java b/src/main/java/org/gridsuite/modification/server/NetworkModificationOnCaseController.java new file mode 100644 index 000000000..b385294ba --- /dev/null +++ b/src/main/java/org/gridsuite/modification/server/NetworkModificationOnCaseController.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2026, 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.modification.server; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.gridsuite.modification.server.service.NetworkModificationOnCaseService; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.UUID; + +/** + * @author Franck Lecuyer + */ +@RestController +@RequestMapping(value = "/" + NetworkModificationApi.API_VERSION + "/cases") +@Tag(name = "network-modification-server on case") +public class NetworkModificationOnCaseController { + + private final NetworkModificationOnCaseService networkModificationOnCaseService; + + public NetworkModificationOnCaseController(NetworkModificationOnCaseService networkModificationOnCaseService) { + this.networkModificationOnCaseService = networkModificationOnCaseService; + } + + @PostMapping(value = "/{caseUuid}/network-composite-modifications", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Apply a list of composite modifications on a case") + @ApiResponse(responseCode = "200", description = "Composite modifications applied on case") + public ResponseEntity applyNetworkCompositeModificationsOnCase(@Parameter(description = "Case UUID") @PathVariable("caseUuid") UUID caseUuid, + @Parameter(description = "Execution UUID") @RequestParam(name = "executionUuid", required = false) UUID executionUuid, + @Parameter(description = "Composite modifications uuids list") @RequestParam("uuids") List compositeModificationUuids) { + networkModificationOnCaseService.applyNetworkCompositeModificationsOnCase(caseUuid, executionUuid, compositeModificationUuids); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/org/gridsuite/modification/server/service/CaseResultInfos.java b/src/main/java/org/gridsuite/modification/server/service/CaseResultInfos.java new file mode 100644 index 000000000..b119ae003 --- /dev/null +++ b/src/main/java/org/gridsuite/modification/server/service/CaseResultInfos.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2026, 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.modification.server.service; + +import lombok.Getter; + +import java.util.UUID; + +/** + * @author Franck Lecuyer + */ +@Getter +public class CaseResultInfos { + private final UUID caseResultUuid; + + private final UUID executionUuid; + + private final UUID reportUuid; + + private final UUID resultUuid; + + private final String stepType; + + private final String status; + + public CaseResultInfos(UUID caseResultUuid, UUID executionUuid, UUID reportUuid, UUID resultUuid, String stepType, String status) { + this.caseResultUuid = caseResultUuid; + this.executionUuid = executionUuid; + this.reportUuid = reportUuid; + this.resultUuid = resultUuid; + this.stepType = stepType; + this.status = status; + } +} diff --git a/src/main/java/org/gridsuite/modification/server/service/NetworkConversionService.java b/src/main/java/org/gridsuite/modification/server/service/NetworkConversionService.java new file mode 100644 index 000000000..769b72767 --- /dev/null +++ b/src/main/java/org/gridsuite/modification/server/service/NetworkConversionService.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2026, 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.modification.server.service; + +import com.powsybl.cases.datasource.CaseDataSourceClient; +import com.powsybl.commons.PowsyblException; +import com.powsybl.commons.report.ReportNode; +import com.powsybl.computation.local.LocalComputationManager; +import com.powsybl.iidm.network.Importer; +import com.powsybl.iidm.network.Network; +import com.powsybl.iidm.network.NetworkFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.Properties; +import java.util.UUID; + +/** + * @author Franck Lecuyer + */ +@Service +public class NetworkConversionService { + private static final Logger LOGGER = LoggerFactory.getLogger(NetworkConversionService.class); + + private final RestTemplate caseServerRest; + + public NetworkConversionService(@Value("${powsybl.services.case-server.base-uri:http://case-server/}") String caseServerBaseUri, + RestTemplateBuilder restTemplateBuilder) { + this.caseServerRest = restTemplateBuilder.rootUri(caseServerBaseUri).build(); + } + + public Network createNetwork(UUID caseUuid, ReportNode reporter) { + LOGGER.info("Creating network"); + + CaseDataSourceClient dataSource = new CaseDataSourceClient(caseServerRest, caseUuid); + + Importer importer = Importer.find(dataSource, LocalComputationManager.getDefault()); + if (importer == null) { + throw new PowsyblException("No importer found"); + } else { + return importer.importData(dataSource, NetworkFactory.findDefault(), new Properties(), reporter); + } + } +} diff --git a/src/main/java/org/gridsuite/modification/server/service/NetworkModificationOnCaseService.java b/src/main/java/org/gridsuite/modification/server/service/NetworkModificationOnCaseService.java new file mode 100644 index 000000000..8434621c1 --- /dev/null +++ b/src/main/java/org/gridsuite/modification/server/service/NetworkModificationOnCaseService.java @@ -0,0 +1,168 @@ +/* + Copyright (c) 2026, 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.modification.server.service; + +import com.powsybl.commons.PowsyblException; +import com.powsybl.commons.datasource.MemDataSource; +import com.powsybl.commons.report.ReportNode; +import com.powsybl.iidm.network.Network; +import org.gridsuite.modification.NetworkModificationException; +import org.gridsuite.modification.dto.ModificationInfos; +import org.gridsuite.modification.modifications.AbstractModification; +import org.gridsuite.modification.server.dto.ReportMode; +import org.gridsuite.modification.server.repositories.NetworkModificationRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +/** + * @author Franck Lecuyer + */ +@Service +public class NetworkModificationOnCaseService { + private static final Logger LOGGER = LoggerFactory.getLogger(NetworkModificationOnCaseService.class); + + private final NetworkModificationRepository networkModificationRepository; + + private final NetworkConversionService networkConversionService; + + private final FilterService filterService; + + private final ReportService reportService; + + private final NotificationService notificationService; + + private final RestTemplate restTemplate; + + private final String caseExportFormat = "XIIDM"; + + public NetworkModificationOnCaseService(NetworkModificationRepository networkModificationRepository, + NetworkConversionService networkConversionService, + FilterService filterService, + NotificationService notificationService, + ReportService reportService, + RestTemplateBuilder restTemplateBuilder, + @Value("${powsybl.services.case-server.base-uri:http://case-server}") String caseServerBaseUri) { + this.networkModificationRepository = networkModificationRepository; + this.networkConversionService = networkConversionService; + this.filterService = filterService; + this.notificationService = notificationService; + this.reportService = reportService; + this.restTemplate = restTemplateBuilder.rootUri(caseServerBaseUri).build(); + } + + private Network loadNetworkFromCase(UUID caseUuid, ReportNode reportNode) { + return networkConversionService.createNetwork(caseUuid, reportNode); + } + + private List getModificationsFromCompositeModifications(List compositeModificationUuids) { + return networkModificationRepository.getCompositeModificationsInfos(compositeModificationUuids); + } + + private void applyModifications(Network network, List modificationsInfos, ReportNode reportNode, FilterService filterService) { + modificationsInfos.stream() + .filter(ModificationInfos::getActivated) + .forEach(modificationInfos -> { + try { + AbstractModification modification = modificationInfos.toModification(); + modification.check(network); + modification.initApplicationContext(filterService, null); + modification.apply(network, reportNode); + } catch (Exception e) { + // For now, we just log the error, and we continue to apply the following modifications + handleException(modificationInfos.getErrorType(), e); + } + }); + } + + private UUID save(Resource resource) { + String uri = "/v1/cases"; + + MultiValueMap body = new LinkedMultiValueMap<>(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + body.add("file", resource); + HttpEntity> request = new HttpEntity<>(body, headers); + + return restTemplate.postForObject(uri, request, UUID.class); + } + + private UUID save(Network network) throws IOException { + MemDataSource memDataSource = new MemDataSource(); + network.write(this.caseExportFormat, null, memDataSource); + + Set listNames = memDataSource.listNames(".*"); + String caseFileName = "apply-modifications-output." + this.caseExportFormat.toLowerCase(); + return save(new ByteArrayResource(memDataSource.getData(listNames.toArray()[0].toString())) { + @Override + public String getFilename() { + return caseFileName; + } + }); + } + + private void handleException(NetworkModificationException.Type typeIfError, Exception e) { + boolean isApplicationException = PowsyblException.class.isAssignableFrom(e.getClass()); + if (!isApplicationException) { + LOGGER.error("{}", e.getMessage(), e); + } else { + LOGGER.error("{} : {}", typeIfError.name(), e.getMessage(), e); + } + } + + public void applyNetworkCompositeModificationsOnCase(UUID caseUuid, UUID executionUuid, List compositeModificationUuids) { + UUID resultCaseUuid = null; + UUID reportUuid = null; + String status = "COMPLETED"; + + try { + ReportNode rootReport = ReportNode.newRootReportNode() + .withAllResourceBundlesFromClasspath() + .withMessageTemplate("network.modification.server.caseUuid") + .withUntypedValue("caseUuid", caseUuid.toString()) + .build(); + + LOGGER.info("Applying modifications on case {}", caseUuid); + + // create network from case + Network network = loadNetworkFromCase(caseUuid, rootReport); + + // get modifications from composite modifications + List modifications = getModificationsFromCompositeModifications(compositeModificationUuids); + + // apply modifications + applyModifications(network, modifications, rootReport, filterService); + + // send report to report server + reportUuid = UUID.randomUUID(); + reportService.sendReport(reportUuid, rootReport, ReportMode.APPEND); + + // save network in case server + resultCaseUuid = save(network); + } catch (Exception e) { + status = "FAILED"; + } finally { + notificationService.sendMessage(MessageBuilder.withPayload(new CaseResultInfos(resultCaseUuid, executionUuid, reportUuid, null, "APPLY_MODIFICATIONS", status)).build(), "publishCaseResult-out-0"); + } + } +} diff --git a/src/main/java/org/gridsuite/modification/server/service/NotificationService.java b/src/main/java/org/gridsuite/modification/server/service/NotificationService.java index 0b29688b8..9241a2737 100644 --- a/src/main/java/org/gridsuite/modification/server/service/NotificationService.java +++ b/src/main/java/org/gridsuite/modification/server/service/NotificationService.java @@ -36,7 +36,7 @@ public class NotificationService { @Autowired private StreamBridge publisher; - private void sendMessage(Message message, String bindingName) { + public void sendMessage(Message message, String bindingName) { OUTPUT_MESSAGE_LOGGER.debug("Sending message : {}", message); publisher.send(bindingName, message); } diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index a309a59ce..89a23e694 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -5,6 +5,8 @@ powsybl: services: network-store-server: base-uri: http://localhost:8080 + case-server: + base-uri: http://localhost:5000 gridsuite: services: diff --git a/src/main/resources/config/application.yaml b/src/main/resources/config/application.yaml index 2edfd845d..6fe6a3c07 100644 --- a/src/main/resources/config/application.yaml +++ b/src/main/resources/config/application.yaml @@ -37,7 +37,9 @@ spring: destination: ${powsybl-ws.rabbitmq.destination.prefix:}build.cancel publishStoppedBuild-out-0: destination: ${powsybl-ws.rabbitmq.destination.prefix:}build.stopped - output-bindings: publishBuild-out-0;publishResultBuild-out-0;publishCancelBuild-out-0;publishStoppedBuild-out-0 + publishCaseResult-out-0: + destination: ${powsybl-ws.rabbitmq.destination.prefix:}modifications.case.result + output-bindings: publishBuild-out-0;publishResultBuild-out-0;publishCancelBuild-out-0;publishStoppedBuild-out-0;publishCaseResult-out-0 rabbit: bindings: consumeBuild-in-0: diff --git a/src/main/resources/org/gridsuite/modification/server/reports.properties b/src/main/resources/org/gridsuite/modification/server/reports.properties index 52619d21d..5e16d0e51 100644 --- a/src/main/resources/org/gridsuite/modification/server/reports.properties +++ b/src/main/resources/org/gridsuite/modification/server/reports.properties @@ -1,2 +1,3 @@ network.modification.server.errorMessage = ${errorMessage} network.modification.server.nodeUuid = ${nodeUuid} +network.modification.server.caseUuid = ${caseUuid} From 833f8b8d4a177845fd27b4d15222b3e52e3e4be5 Mon Sep 17 00:00:00 2001 From: Franck LECUYER Date: Tue, 24 Feb 2026 17:34:28 +0100 Subject: [PATCH 2/2] Fix failing existing unit tests Signed-off-by: Franck LECUYER --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 41786155b..15a118931 100644 --- a/pom.xml +++ b/pom.xml @@ -68,6 +68,7 @@ com.powsybl:powsybl-config-classic + com.powsybl:powsybl-iidm-impl