Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
<configuration>
<classpathDependencyExcludes>
<classpathDependencyExclude>com.powsybl:powsybl-config-classic</classpathDependencyExclude>
<classpathDependencyExclude>com.powsybl:powsybl-iidm-impl</classpathDependencyExclude>
</classpathDependencyExcludes>
</configuration>
</plugin>
Expand Down Expand Up @@ -162,6 +163,10 @@
<groupId>com.powsybl</groupId>
<artifactId>powsybl-iidm-api</artifactId>
</dependency>
<dependency>
<groupId>com.powsybl</groupId>
<artifactId>powsybl-iidm-impl</artifactId>
</dependency>
<dependency>
<groupId>com.powsybl</groupId>
<artifactId>powsybl-iidm-modification</artifactId>
Expand Down Expand Up @@ -199,6 +204,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.powsybl</groupId>
<artifactId>powsybl-case-datasource-client</artifactId>
</dependency>

<!-- elasticsearch -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <franck.lecuyer at rte-france.com>
*/
@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<Void> 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<UUID> compositeModificationUuids) {
networkModificationOnCaseService.applyNetworkCompositeModificationsOnCase(caseUuid, executionUuid, compositeModificationUuids);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -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 <franck.lecuyer at rte-france.com>
*/
@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;
}
}
Original file line number Diff line number Diff line change
@@ -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 <franck.lecuyer at rte-france.com>
*/
@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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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 <franck.lecuyer at rte-france.com>
*/
@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";

Check warning on line 57 in src/main/java/org/gridsuite/modification/server/service/NetworkModificationOnCaseService.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make this final field static too.

See more on https://sonarcloud.io/project/issues?id=org.gridsuite%3Anetwork-modification-server&issues=AZyQhoxrZyB6hK62xFZ-&open=AZyQhoxrZyB6hK62xFZ-&pullRequest=771

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<ModificationInfos> getModificationsFromCompositeModifications(List<UUID> compositeModificationUuids) {
return networkModificationRepository.getCompositeModificationsInfos(compositeModificationUuids);
}

private void applyModifications(Network network, List<ModificationInfos> 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<String, Object> body = new LinkedMultiValueMap<>();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
body.add("file", resource);
HttpEntity<MultiValueMap<String, Object>> 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<String> 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;
}
});
}
Comment on lines +110 to +122
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential ArrayIndexOutOfBoundsException if listNames is empty.

If memDataSource.listNames(".*") returns an empty set, accessing toArray()[0] will throw ArrayIndexOutOfBoundsException. Add a defensive check or use a safer access pattern.

🛡️ Suggested fix
     private UUID save(Network network) throws IOException {
         MemDataSource memDataSource = new MemDataSource();
         network.write(this.caseExportFormat, null, memDataSource);
 
         Set<String> listNames = memDataSource.listNames(".*");
+        if (listNames.isEmpty()) {
+            throw new PowsyblException("Network serialization produced no output files");
+        }
         String caseFileName = "apply-modifications-output." + this.caseExportFormat.toLowerCase();
-        return save(new ByteArrayResource(memDataSource.getData(listNames.toArray()[0].toString())) {
+        String outputName = listNames.iterator().next();
+        return save(new ByteArrayResource(memDataSource.getData(outputName)) {
             `@Override`
             public String getFilename() {
                 return caseFileName;
             }
         });
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private UUID save(Network network) throws IOException {
MemDataSource memDataSource = new MemDataSource();
network.write(this.caseExportFormat, null, memDataSource);
Set<String> 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 UUID save(Network network) throws IOException {
MemDataSource memDataSource = new MemDataSource();
network.write(this.caseExportFormat, null, memDataSource);
Set<String> listNames = memDataSource.listNames(".*");
if (listNames.isEmpty()) {
throw new PowsyblException("Network serialization produced no output files");
}
String caseFileName = "apply-modifications-output." + this.caseExportFormat.toLowerCase();
String outputName = listNames.iterator().next();
return save(new ByteArrayResource(memDataSource.getData(outputName)) {
`@Override`
public String getFilename() {
return caseFileName;
}
});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/gridsuite/modification/server/service/NetworkModificationOnCaseService.java`
around lines 110 - 122, In save(Network network) guard against
memDataSource.listNames(".*") returning empty: check if listNames.isEmpty() and
throw an IOException (or a specific checked exception) with a clear message
before using toArray()[0]; then replace the fragile toArray()[0] access with a
safe retrieval (e.g., listNames.iterator().next() or
listNames.stream().findFirst().get()) and continue creating the
ByteArrayResource with caseFileName and delegate to save(...). Ensure you
reference the memDataSource, listNames, caseExportFormat, ByteArrayResource and
the save(...) call when making the change.


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<UUID> 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<ModificationInfos> 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";
Comment on lines +162 to +163
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Exception is caught but silently discarded without logging.

The caught exception e is never logged or recorded, making it impossible to diagnose failures in production. At minimum, log the exception details.

🐛 Proposed fix
         } catch (Exception e) {
+            LOGGER.error("Failed to apply modifications on case {}: {}", caseUuid, e.getMessage(), e);
             status = "FAILED";
         } finally {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (Exception e) {
status = "FAILED";
} catch (Exception e) {
LOGGER.error("Failed to apply modifications on case {}: {}", caseUuid, e.getMessage(), e);
status = "FAILED";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/gridsuite/modification/server/service/NetworkModificationOnCaseService.java`
around lines 162 - 163, The catch block in NetworkModificationOnCaseService that
sets status = "FAILED" currently swallows the exception; update that catch to
log the exception (including stacktrace) using the class logger (e.g.,
logger.error("Failed to process network modification for case {}: {}",
caseIdOrContext, e.getMessage(), e)) so the error and context are recorded;
ensure you reference the existing logger field in
NetworkModificationOnCaseService and include any relevant identifiers (case
id/tenant id) available in the method to aid troubleshooting.

} finally {
notificationService.sendMessage(MessageBuilder.withPayload(new CaseResultInfos(resultCaseUuid, executionUuid, reportUuid, null, "APPLY_MODIFICATIONS", status)).build(), "publishCaseResult-out-0");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ powsybl:
services:
network-store-server:
base-uri: http://localhost:8080
case-server:
base-uri: http://localhost:5000

gridsuite:
services:
Expand Down
4 changes: 3 additions & 1 deletion src/main/resources/config/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
network.modification.server.errorMessage = ${errorMessage}
network.modification.server.nodeUuid = ${nodeUuid}
network.modification.server.caseUuid = ${caseUuid}
Loading