-
Notifications
You must be signed in to change notification settings - Fork 0
POC : apply modifications on a case (to be used by monitor-core) #771
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bc6339a
71e6e08
833f8b8
d85f408
17f5140
e3e4e44
535e0fc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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
|
||||||||||||
|
|
||||||||||||
| 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; | ||||||||||||
| } | ||||||||||||
| }); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: Exception is caught but silently discarded without logging. The caught exception 🐛 Proposed fix } catch (Exception e) {
+ LOGGER.error("Failed to apply modifications on case {}: {}", caseUuid, e.getMessage(), e);
status = "FAILED";
} finally {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
| } 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 |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| network.modification.server.errorMessage = ${errorMessage} | ||
| network.modification.server.nodeUuid = ${nodeUuid} | ||
| network.modification.server.caseUuid = ${caseUuid} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential
ArrayIndexOutOfBoundsExceptioniflistNamesis empty.If
memDataSource.listNames(".*")returns an empty set, accessingtoArray()[0]will throwArrayIndexOutOfBoundsException. 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
🤖 Prompt for AI Agents