diff --git a/pom.xml b/pom.xml
index acf163716..16d4ead4f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -68,6 +68,7 @@
com.powsybl:powsybl-config-classic
+ com.powsybl:powsybl-iidm-impl
@@ -162,6 +163,10 @@
com.powsybl
powsybl-iidm-api
+
+ com.powsybl
+ powsybl-iidm-impl
+
com.powsybl
powsybl-iidm-modification
@@ -199,6 +204,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 extends Object> message, String bindingName) {
+ public void sendMessage(Message extends Object> 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}