Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8894fcb
feat: when running loadflow on security node, all its children are bu…
klesaulnier Dec 29, 2025
05e9350
feat: on security node creation, with construction node as parent, bu…
klesaulnier Dec 29, 2025
2ded25a
feat: add auto rebuild on multiple operations
klesaulnier Dec 30, 2025
b2e27c5
Merge branch 'main' into security-node-auto-build
klesaulnier Dec 30, 2025
e34cfc5
fix: checkstyle and copyright
klesaulnier Dec 30, 2025
e1da708
fix: replace aspect with handler
klesaulnier Dec 31, 2025
eb3db27
fix: create modification on node with loadflow
klesaulnier Jan 2, 2026
890bbe4
fix: handle modification moves
klesaulnier Jan 2, 2026
26fb59a
Merge remote-tracking branch 'origin/main' into security-node-auto-build
klesaulnier Jan 2, 2026
5b2bb94
fix: some tests
klesaulnier Jan 5, 2026
a066011
fix: all tests
klesaulnier Jan 6, 2026
5f1b820
test: cover controller
klesaulnier Jan 6, 2026
ed45367
test: cover and checkstyle
klesaulnier Jan 6, 2026
3a8ca5a
Merge branch 'main' into security-node-auto-build
klesaulnier Jan 6, 2026
5f33fb1
test: improve coverage
klesaulnier Jan 6, 2026
4b1acae
fix: checkstyle
klesaulnier Jan 6, 2026
b46fcf6
fix: sonar issues
klesaulnier Jan 6, 2026
49427ca
fix: small fixes
klesaulnier Jan 6, 2026
7e116eb
fix: PR remarks
klesaulnier Jan 12, 2026
9afba41
Merge remote-tracking branch 'origin/main' into security-node-auto-build
klesaulnier Jan 12, 2026
4b6c5c2
fix: checkstyle
klesaulnier Jan 12, 2026
5803033
Review
Jan 15, 2026
2ed9281
fix: PR remarks
klesaulnier Jan 19, 2026
a0b01ad
Merge branch 'main' into security-node-auto-build
klesaulnier Jan 19, 2026
028a797
fix: tests
klesaulnier Jan 19, 2026
f74f6ba
fix: copyright
klesaulnier Jan 19, 2026
4986526
fix: move post build action in separate transactions
klesaulnier Jan 19, 2026
305bfa5
fix: sonar issues
klesaulnier Jan 19, 2026
9ccf1b5
Merge branch 'main' into security-node-auto-build
klesaulnier Jan 19, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@
import org.apache.commons.lang3.StringUtils;
import org.gridsuite.filter.globalfilter.GlobalFilter;
import org.gridsuite.filter.utils.EquipmentType;
import org.gridsuite.study.server.service.RebuildNodeService;
import org.gridsuite.study.server.StudyApi;
import org.gridsuite.study.server.StudyConstants.ModificationsActionType;
import org.gridsuite.study.server.dto.modification.NetworkModificationMetadata;
import org.gridsuite.study.server.error.StudyException;
import org.gridsuite.study.server.dto.*;
import org.gridsuite.study.server.dto.computation.LoadFlowComputationInfos;
import org.gridsuite.study.server.dto.diagramgridlayout.DiagramGridLayout;
Expand All @@ -35,13 +33,15 @@
import org.gridsuite.study.server.dto.elasticsearch.EquipmentInfos;
import org.gridsuite.study.server.dto.modification.ModificationType;
import org.gridsuite.study.server.dto.modification.ModificationsSearchResultByNode;
import org.gridsuite.study.server.dto.modification.NetworkModificationMetadata;
import org.gridsuite.study.server.dto.networkexport.ExportNetworkStatus;
import org.gridsuite.study.server.dto.sensianalysis.SensitivityAnalysisCsvFileInfos;
import org.gridsuite.study.server.dto.sequence.NodeSequenceType;
import org.gridsuite.study.server.dto.timeseries.TimeSeriesMetadataInfos;
import org.gridsuite.study.server.dto.timeseries.TimelineEventInfos;
import org.gridsuite.study.server.dto.voltageinit.parameters.StudyVoltageInitParameters;
import org.gridsuite.study.server.elasticsearch.EquipmentInfosService;
import org.gridsuite.study.server.error.StudyException;
import org.gridsuite.study.server.exception.PartialResultException;
import org.gridsuite.study.server.networkmodificationtree.dto.*;
import org.gridsuite.study.server.service.*;
Expand Down Expand Up @@ -88,6 +88,7 @@ public class StudyController {
private final RootNetworkService rootNetworkService;
private final RootNetworkNodeInfoService rootNetworkNodeInfoService;
private final SensitivityAnalysisService sensitivityAnalysisService;
private final RebuildNodeService rebuildNodeService;

public StudyController(StudyService studyService,
NetworkService networkStoreService,
Expand All @@ -97,7 +98,9 @@ public StudyController(StudyService studyService,
CaseService caseService,
RemoteServicesInspector remoteServicesInspector,
RootNetworkService rootNetworkService,
RootNetworkNodeInfoService rootNetworkNodeInfoService, SensitivityAnalysisService sensitivityAnalysisService) {
RootNetworkNodeInfoService rootNetworkNodeInfoService,
SensitivityAnalysisService sensitivityAnalysisService,
RebuildNodeService rebuildNodeService) {
this.studyService = studyService;
this.networkModificationTreeService = networkModificationTreeService;
this.networkStoreService = networkStoreService;
Expand All @@ -108,6 +111,7 @@ public StudyController(StudyService studyService,
this.rootNetworkService = rootNetworkService;
this.rootNetworkNodeInfoService = rootNetworkNodeInfoService;
this.sensitivityAnalysisService = sensitivityAnalysisService;
this.rebuildNodeService = rebuildNodeService;
}

@InitBinder
Expand Down Expand Up @@ -639,18 +643,9 @@ public ResponseEntity<Void> moveModification(@PathVariable("studyUuid") UUID stu
@Nullable @Parameter(description = "move before, if no value move to end") @RequestParam(value = "beforeUuid") UUID beforeUuid,
@RequestHeader(HEADER_USER_ID) String userId) {
studyService.assertCanUpdateModifications(studyUuid, nodeUuid);
handleMoveNetworkModification(studyUuid, nodeUuid, modificationUuid, beforeUuid, userId);
return ResponseEntity.ok().build();
}

private void handleMoveNetworkModification(UUID studyUuid, UUID nodeUuid, UUID modificationUuid, UUID beforeUuid, String userId) {
studyService.assertNoBlockedNodeInStudy(studyUuid, nodeUuid);
studyService.invalidateNodeTreeWhenMoveModification(studyUuid, nodeUuid);
try {
studyService.moveNetworkModifications(studyUuid, nodeUuid, nodeUuid, List.of(modificationUuid), beforeUuid, false, userId);
} finally {
studyService.unblockNodeTree(studyUuid, nodeUuid);
}
rebuildNodeService.moveNetworkModification(studyUuid, nodeUuid, modificationUuid, beforeUuid, userId);
return ResponseEntity.ok().build();
}

@PutMapping(value = "/studies/{studyUuid}/nodes/{nodeUuid}", produces = MediaType.APPLICATION_JSON_VALUE)
Expand All @@ -675,7 +670,9 @@ public ResponseEntity<Void> moveOrCopyModifications(@PathVariable("studyUuid") U
if (!studyUuid.equals(originStudyUuid)) {
throw new StudyException(MOVE_NETWORK_MODIFICATION_FORBIDDEN);
}
handleMoveNetworkModifications(studyUuid, nodeUuid, originNodeUuid, modificationsToCopyUuidList, userId);
studyService.assertNoBlockedNodeInStudy(studyUuid, originNodeUuid);
studyService.assertNoBlockedNodeInStudy(studyUuid, nodeUuid);
rebuildNodeService.moveNetworkModifications(studyUuid, nodeUuid, originNodeUuid, modificationsToCopyUuidList, userId);
break;
}
return ResponseEntity.ok().build();
Expand All @@ -691,20 +688,6 @@ private void handleDuplicateOrInsertNetworkModifications(UUID targetStudyUuid, U
}
}

private void handleMoveNetworkModifications(UUID studyUuid, UUID targetNodeUuid, UUID originNodeUuid, List<UUID> modificationsToCopyUuidList, String userId) {
studyService.assertNoBlockedNodeInStudy(studyUuid, originNodeUuid);
studyService.assertNoBlockedNodeInStudy(studyUuid, targetNodeUuid);
boolean isTargetInDifferentNodeTree = studyService.invalidateNodeTreeWhenMoveModifications(studyUuid, targetNodeUuid, originNodeUuid);
try {
studyService.moveNetworkModifications(studyUuid, targetNodeUuid, originNodeUuid, modificationsToCopyUuidList, null, isTargetInDifferentNodeTree, userId);
} finally {
studyService.unblockNodeTree(studyUuid, originNodeUuid);
if (isTargetInDifferentNodeTree) {
studyService.unblockNodeTree(studyUuid, targetNodeUuid);
}
}
}

@PutMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/loadflow/run")
@Operation(summary = "run loadflow on study")
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The loadflow has started")})
Expand Down Expand Up @@ -1363,19 +1346,10 @@ public ResponseEntity<Void> createNetworkModification(@Parameter(description = "
@RequestHeader(HEADER_USER_ID) String userId) {
studyService.assertCanUpdateModifications(studyUuid, nodeUuid);
studyService.assertNoBlockedNodeInStudy(studyUuid, nodeUuid);
handleCreateNetworkModification(studyUuid, nodeUuid, modificationAttributes, userId);
rebuildNodeService.createNetworkModification(studyUuid, nodeUuid, modificationAttributes, userId);
return ResponseEntity.ok().build();
}

private void handleCreateNetworkModification(UUID studyUuid, UUID nodeUuid, String modificationAttributes, String userId) {
studyService.invalidateNodeTreeWithLF(studyUuid, nodeUuid);
try {
studyService.createNetworkModification(studyUuid, nodeUuid, modificationAttributes, userId);
} finally {
studyService.unblockNodeTree(studyUuid, nodeUuid);
}
}

@PutMapping(value = "/studies/{studyUuid}/nodes/{nodeUuid}/network-modifications/{uuid}")
@Operation(summary = "Update a modification in the study network")
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The network modification was updated"), @ApiResponse(responseCode = "404", description = "The study/node is not found")})
Expand All @@ -1386,7 +1360,7 @@ public ResponseEntity<Void> updateNetworkModification(@Parameter(description = "
@RequestHeader(HEADER_USER_ID) String userId) {
studyService.assertCanUpdateModifications(studyUuid, nodeUuid);
studyService.assertNoBlockedNodeInStudy(studyUuid, nodeUuid);
studyService.updateNetworkModification(studyUuid, modificationAttributes, nodeUuid, networkModificationUuid, userId);
rebuildNodeService.updateNetworkModification(studyUuid, modificationAttributes, nodeUuid, networkModificationUuid, userId);
return ResponseEntity.ok().build();
}

Expand Down Expand Up @@ -1414,9 +1388,9 @@ public ResponseEntity<Void> stashNetworkModifications(@Parameter(description = "
studyService.assertCanUpdateModifications(studyUuid, nodeUuid);
studyService.assertNoBlockedNodeInStudy(studyUuid, nodeUuid);
if (stashed.booleanValue()) {
studyService.stashNetworkModifications(studyUuid, nodeUuid, networkModificationUuids, userId);
rebuildNodeService.stashNetworkModifications(studyUuid, nodeUuid, networkModificationUuids, userId);
} else {
studyService.restoreNetworkModifications(studyUuid, nodeUuid, networkModificationUuids, userId);
rebuildNodeService.restoreNetworkModifications(studyUuid, nodeUuid, networkModificationUuids, userId);
}
return ResponseEntity.ok().build();
}
Expand All @@ -1431,7 +1405,7 @@ public ResponseEntity<Void> updateNetworkModificationsMetadata(@Parameter(descri
@RequestHeader(HEADER_USER_ID) String userId) {
studyService.assertCanUpdateModifications(studyUuid, nodeUuid);
studyService.assertNoBlockedNodeInStudy(studyUuid, nodeUuid);
studyService.updateNetworkModificationsMetadata(studyUuid, nodeUuid, networkModificationUuids, userId, metadata);
rebuildNodeService.updateNetworkModificationsMetadata(studyUuid, nodeUuid, networkModificationUuids, userId, metadata);
return ResponseEntity.ok().build();
}

Expand All @@ -1447,7 +1421,7 @@ public ResponseEntity<Void> updateNetworkModificationsActivation(@Parameter(desc
studyService.assertCanUpdateModifications(studyUuid, nodeUuid);
studyService.assertNoBuildNoComputationForRootNetworkNode(nodeUuid, rootNetworkUuid);
studyService.assertNoBlockedNodeInTree(nodeUuid, rootNetworkUuid);
studyService.updateNetworkModificationsActivationInRootNetwork(studyUuid, nodeUuid, rootNetworkUuid, networkModificationUuids, userId, activated);
rebuildNodeService.updateNetworkModificationsActivation(studyUuid, nodeUuid, rootNetworkUuid, networkModificationUuids, userId, activated);
return ResponseEntity.ok().build();
}

Expand Down Expand Up @@ -1503,7 +1477,11 @@ public ResponseEntity<NetworkModificationNode> createNode(@RequestBody NetworkMo
@Parameter(description = "parent id of the node created") @PathVariable(name = "id") UUID referenceId,
@Parameter(description = "node is inserted before the given node ID") @RequestParam(name = "mode", required = false, defaultValue = "CHILD") InsertMode insertMode,
@RequestHeader(HEADER_USER_ID) String userId) {
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(studyService.createNode(studyUuid, referenceId, node, insertMode, userId));

NetworkModificationNode newNode = studyService.createNode(studyUuid, referenceId, node, insertMode, userId);
studyService.createNodePostAction(studyUuid, referenceId, newNode, userId);

return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(newNode);
}

@PostMapping(value = "/studies/{studyUuid}/tree/nodes/{id}", params = {"sequenceType"})
Expand All @@ -1516,7 +1494,9 @@ public ResponseEntity<NetworkModificationNode> createSequence(
@Parameter(description = "parent id of the node created") @PathVariable(name = "id") UUID referenceId,
@Parameter(description = "sequence to create") @RequestParam("sequenceType") NodeSequenceType nodeSequenceType,
@RequestHeader(HEADER_USER_ID) String userId) {
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(studyService.createSequence(studyUuid, referenceId, nodeSequenceType, userId));
NetworkModificationNode sequenceParentNode = studyService.createSequence(studyUuid, referenceId, nodeSequenceType, userId);
studyService.createSequencePostAction(studyUuid, referenceId, nodeSequenceType, userId);
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(sequenceParentNode);
}

@DeleteMapping(value = "/studies/{studyUuid}/tree/nodes")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,25 @@ public interface NodeRepository extends JpaRepository<NodeEntity, UUID> {
"WHERE n.id_node IN (SELECT nh.id_node FROM NodeHierarchy nh) AND n.id_node != :nodeUuid")
List<NodeEntity> findAllChildren(UUID nodeUuid);

@NativeQuery("WITH RECURSIVE ancestors (id_node, parent_node) AS ( " +
" SELECT n.id_node, n.parent_node " +
" FROM NODE n " +
" WHERE n.id_node = :childNodeUuid " +

" UNION ALL " +

" SELECT p.id_node, p.parent_node " +
" FROM NODE p " +
" INNER JOIN ancestors a ON p.id_node = a.parent_node " +
") " +
"SELECT EXISTS ( " +
" SELECT 1 " +
" FROM ancestors " +
" WHERE id_node = :ancestorNodeUuid " +
")"
)
boolean isAncestor(UUID ancestorNodeUuid, UUID childNodeUuid);

List<NodeEntity> findAllByStudyIdAndStashedAndParentNodeIdNodeOrderByStashDateDesc(UUID id, boolean stashed, UUID parentNode);

Optional<NodeEntity> findByStudyIdAndType(UUID id, NodeType type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -641,13 +641,28 @@ public void consumeCalculationResult(Message<String> msg, ComputationType comput
// unblock node
handleUnblockNode(receiverObj, computationType);

// send notifications
UUID studyUuid = networkModificationTreeService.getStudyUuidForNodeId(receiverObj.getNodeUuid());
if (computationType == LOAD_FLOW) {
String userId = (String) msg.getHeaders().get(HEADER_USER_ID);
handleLoadFlowSuccess(studyUuid, receiverObj.getNodeUuid(), receiverObj.getRootNetworkUuid(), resultUuid, userId);
}

// send notifications
notificationService.emitStudyChanged(studyUuid, receiverObj.getNodeUuid(), receiverObj.getRootNetworkUuid(), computationType.getUpdateStatusType());
notificationService.emitStudyChanged(studyUuid, receiverObj.getNodeUuid(), receiverObj.getRootNetworkUuid(), computationType.getUpdateResultType());
}));
}

private void handleLoadFlowSuccess(UUID studyUuid, UUID nodeUuid, UUID rootNetworkUuid, UUID resultUuid, String userId) {
// Build 1st level children if loadflow is converged, and node is a security type
if (userId != null && networkModificationTreeService.isSecurityNode(nodeUuid)) {
LoadFlowStatus loadFlowStatus = loadFlowService.getLoadFlowStatus(resultUuid);
if (loadFlowStatus == LoadFlowStatus.CONVERGED) {
studyService.buildFirstLevelChildren(studyUuid, nodeUuid, rootNetworkUuid, userId);
}
}
}

Optional<NodeReceiver> getNodeReceiver(Message<String> msg) {
String receiver = msg.getHeaders().get(HEADER_RECEIVER, String.class);
if (Strings.isBlank(receiver)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -599,11 +599,15 @@ private void assertNodeNameNotExist(UUID studyUuid, String nodeName) {
}

public void assertIsRootOrConstructionNode(UUID nodeUuid) {
if (!self.getNode(nodeUuid, null).getType().equals(NodeType.ROOT) && !isConstructionNode(nodeUuid)) {
if (!isRootOrConstructionNode(nodeUuid)) {
throw new StudyException(NOT_ALLOWED);
}
}

public boolean isRootOrConstructionNode(UUID nodeUuid) {
return getNodeEntity(nodeUuid).getType() == NodeType.ROOT || isConstructionNode(nodeUuid);
}

private void assertInsertNode(
UUID parentNodeId,
NetworkModificationNodeType newNodeType,
Expand Down Expand Up @@ -1264,6 +1268,23 @@ public NetworkModificationNode createNodeTree(@NonNull StudyEntity study, @NonNu
return nodeInfo;
}

@Transactional
public List<UUID> getHighestNodeUuids(UUID node1Uuid, UUID node2Uuid) {
if (node1Uuid.equals(node2Uuid)) {
return List.of(node1Uuid);
}

if (nodesRepository.isAncestor(node1Uuid, node2Uuid)) {
return List.of(node1Uuid);
}

if (nodesRepository.isAncestor(node2Uuid, node1Uuid)) {
return List.of(node2Uuid);
}

return List.of(node1Uuid, node2Uuid);
}

@Transactional
public void updateExportNetworkStatus(UUID nodeUuid, UUID exportUuid, ExportNetworkStatus status) {
nodesRepository.getReferenceById(nodeUuid).getNodeExportNetwork().add(NodeExportEmbeddable.toNodeExportEmbeddable(exportUuid, status));
Expand Down
Loading