diff --git a/src/main/java/org/gridsuite/study/server/controller/StudyController.java b/src/main/java/org/gridsuite/study/server/controller/StudyController.java index a09335b21..f068b3cb2 100644 --- a/src/main/java/org/gridsuite/study/server/controller/StudyController.java +++ b/src/main/java/org/gridsuite/study/server/controller/StudyController.java @@ -24,6 +24,7 @@ import org.gridsuite.study.server.dto.computation.LoadFlowComputationInfos; import org.gridsuite.study.server.dto.dynamicsimulation.event.EventInfos; import org.gridsuite.study.server.dto.elasticsearch.EquipmentInfos; +import org.gridsuite.study.server.dto.modification.CompositesToBeInserted; import org.gridsuite.study.server.dto.modification.ModificationType; import org.gridsuite.study.server.dto.modification.ModificationsSearchResultByNode; import org.gridsuite.study.server.dto.modification.NetworkModificationMetadata; @@ -178,8 +179,9 @@ public ResponseEntity getStudy(@PathVariable("studyUuid" @DeleteMapping(value = "/studies/{studyUuid}") @Operation(summary = "delete the study") @ApiResponse(responseCode = "200", description = "Study deleted") - public ResponseEntity deleteStudy(@PathVariable("studyUuid") UUID studyUuid) { - studyService.deleteStudyIfNotCreationInProgress(studyUuid); + public ResponseEntity deleteStudy(@PathVariable("studyUuid") UUID studyUuid, + @RequestHeader(HEADER_USER_ID) String userId) { + studyService.deleteStudyIfNotCreationInProgress(studyUuid, userId); return ResponseEntity.ok().build(); } @@ -727,29 +729,30 @@ public ResponseEntity assembleModificationsIntoNewComposite( return ResponseEntity.ok().body(newCompositeUuid); } - /** - * @param modificationsToInsert pair of the composite uuid and its name - */ @PutMapping(value = "/studies/{studyUuid}/nodes/{nodeUuid}/composite-modifications", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "For a list of composite network modifications passed in body, insert them into the target node") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The composite modification list has been inserted.")}) public ResponseEntity insertCompositeModifications(@PathVariable("studyUuid") UUID studyUuid, @PathVariable("nodeUuid") UUID nodeUuid, @RequestParam("action") CompositeModificationsActionType action, - @RequestBody List> modificationsToInsert, + @RequestBody List compositesToBeInserted, @RequestHeader(HEADER_USER_ID) String userId) { studyService.assertIsStudyAndNodeExist(studyUuid, nodeUuid); studyService.assertCanUpdateNodeInStudy(studyUuid, nodeUuid); - handleInsertCompositeNetworkModifications(studyUuid, nodeUuid, modificationsToInsert, userId, action); + handleInsertCompositeNetworkModifications(studyUuid, nodeUuid, compositesToBeInserted, userId, action); return ResponseEntity.ok().build(); } - private void handleInsertCompositeNetworkModifications(UUID targetStudyUuid, UUID targetNodeUuid, List> modificationsToCopy, String userId, + private void handleInsertCompositeNetworkModifications( + UUID targetStudyUuid, + UUID targetNodeUuid, + List compositesToBeInserted, + String userId, CompositeModificationsActionType action) { studyService.assertNoBlockedNodeInStudy(targetStudyUuid, targetNodeUuid); studyService.invalidateNodeTreeWithLF(targetStudyUuid, targetNodeUuid); try { - studyService.insertCompositeNetworkModifications(targetStudyUuid, targetNodeUuid, modificationsToCopy, userId, action); + studyService.insertCompositeNetworkModifications(targetStudyUuid, targetNodeUuid, compositesToBeInserted, userId, action); } finally { studyService.unblockNodeTree(targetStudyUuid, targetNodeUuid); } @@ -1454,7 +1457,7 @@ public ResponseEntity deleteNetworkModifications(@Parameter(description = } @PutMapping(value = "/studies/{studyUuid}/nodes/{nodeUuid}/network-modifications", params = "stashed") - @Operation(summary = "Stash network modifications for a node") + @Operation(summary = "Stash or restore network modifications for a node") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The network modifications were stashed / restored "), @ApiResponse(responseCode = "404", description = "The study/node is not found")}) public ResponseEntity stashNetworkModifications(@Parameter(description = "Study UUID") @PathVariable("studyUuid") UUID studyUuid, diff --git a/src/main/java/org/gridsuite/study/server/dto/DeleteStudyInfos.java b/src/main/java/org/gridsuite/study/server/dto/DeleteStudyInfos.java index a84124f9d..0379a4ace 100644 --- a/src/main/java/org/gridsuite/study/server/dto/DeleteStudyInfos.java +++ b/src/main/java/org/gridsuite/study/server/dto/DeleteStudyInfos.java @@ -8,6 +8,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; +import org.springframework.data.util.Pair; import java.util.List; import java.util.UUID; @@ -22,5 +23,6 @@ public class DeleteStudyInfos { private List rootNetworkInfosList; - private List modificationGroupUuids; + // group uuids as keys and each their own node as values + private List> modificationGroupUuidsNodeUuids; } diff --git a/src/main/java/org/gridsuite/study/server/dto/ReferenceAttributes.java b/src/main/java/org/gridsuite/study/server/dto/ReferenceAttributes.java new file mode 100644 index 000000000..fcfcbf5e7 --- /dev/null +++ b/src/main/java/org/gridsuite/study/server/dto/ReferenceAttributes.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.study.server.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +import java.util.UUID; + +/** + * @author Mathieu Deharbe + * attributes of the references to the shared composites stored in directory server + */ +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@SuperBuilder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ReferenceAttributes { + public enum ReferenceType { + STUDY_NODE, + NETWORK_MODIFICATION, + DIRECTORY_ELEMENT + } + + private UUID referenceId; + private ReferenceType referenceType; +} + diff --git a/src/main/java/org/gridsuite/study/server/dto/modification/CompositesToBeInserted.java b/src/main/java/org/gridsuite/study/server/dto/modification/CompositesToBeInserted.java new file mode 100644 index 000000000..132854e13 --- /dev/null +++ b/src/main/java/org/gridsuite/study/server/dto/modification/CompositesToBeInserted.java @@ -0,0 +1,15 @@ +/* + 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.study.server.dto.modification; + +import java.util.UUID; + +/** + * @author Mathieu Deharbe + */ +public record CompositesToBeInserted(UUID id, String name, boolean isShared) { } + diff --git a/src/main/java/org/gridsuite/study/server/service/ConsumerService.java b/src/main/java/org/gridsuite/study/server/service/ConsumerService.java index c682995a9..d0f0d1b63 100644 --- a/src/main/java/org/gridsuite/study/server/service/ConsumerService.java +++ b/src/main/java/org/gridsuite/study/server/service/ConsumerService.java @@ -252,7 +252,7 @@ private void handleConsumeCaseImportSucceeded(CaseImportReceiver receiver, UUID } finally { // if studyEntity is already existing, we don't delete anything in the end of the process if (caseImportAction == CaseImportAction.STUDY_CREATION) { - studyService.deleteStudyIfNotCreationInProgress(studyUuid); + studyService.deleteStudyIfNotCreationInProgress(studyUuid, userId); } if (caseImportAction == CaseImportAction.ROOT_NETWORK_MODIFICATION) { UUID rootNodeUuid = networkModificationTreeService.getStudyRootNodeUuid(studyUuid); @@ -352,7 +352,7 @@ public Consumer> consumeCaseImportFailed() { UUID rootNetworkUuid = receiver.getRootNetworkUuid(); if (receiver.getCaseImportAction() == CaseImportAction.STUDY_CREATION) { - studyService.deleteStudyIfNotCreationInProgress(studyUuid); + studyService.deleteStudyIfNotCreationInProgress(studyUuid, userId); notificationService.emitStudyCreationError(studyUuid, userId, errorMessage); } else { if (receiver.getCaseImportAction() == CaseImportAction.ROOT_NETWORK_CREATION) { diff --git a/src/main/java/org/gridsuite/study/server/service/DirectoryService.java b/src/main/java/org/gridsuite/study/server/service/DirectoryService.java index 7512e5e24..e7b3b48a3 100644 --- a/src/main/java/org/gridsuite/study/server/service/DirectoryService.java +++ b/src/main/java/org/gridsuite/study/server/service/DirectoryService.java @@ -7,9 +7,11 @@ package org.gridsuite.study.server.service; import lombok.Getter; +import lombok.NonNull; import lombok.Setter; import org.gridsuite.study.server.RemoteServicesProperties; import org.gridsuite.study.server.dto.ElementAttributes; +import org.gridsuite.study.server.dto.ReferenceAttributes; import org.gridsuite.study.server.dto.networkexport.PermissionType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.ParameterizedTypeReference; @@ -27,6 +29,7 @@ import java.util.*; import static org.gridsuite.study.server.StudyConstants.*; +import static org.gridsuite.study.server.dto.ReferenceAttributes.ReferenceType.STUDY_NODE; /** * @author David Braquart @@ -99,13 +102,60 @@ public boolean elementExists(UUID directoryUuid, String elementName, String type return response.getStatusCode() == HttpStatus.OK; } + /** + * creates references and add them to shared composite modifications stored in directory server + * @param elementsUuids element uuids of the shared composites in directory server + * @param userId id of the user who creates the references + * @param targetNodeUuid where the new references will point + */ + public void createsReferencesToSharedComposites(@NonNull List elementsUuids, String userId, UUID targetNodeUuid) { + elementsUuids.forEach(elementUuid -> { + var path = UriComponentsBuilder.fromPath( + DELIMITER + DIRECTORY_API_VERSION + DELIMITER + "elements/{elementUuid}/references") + .buildAndExpand(elementUuid) + .toUriString(); + + HttpHeaders headers = new HttpHeaders(); + headers.set(HEADER_USER_ID, userId); + headers.setContentType(MediaType.APPLICATION_JSON); + + ReferenceAttributes referenceAttributes = new ReferenceAttributes(targetNodeUuid, STUDY_NODE); + + HttpEntity requestEntity = new HttpEntity<>(referenceAttributes, headers); + restTemplate.exchange(getDirectoryServerServerBaseUri() + path, HttpMethod.POST, requestEntity, ElementAttributes.class); + }); + } + + /** + * remove reference from the shared modification in directory server + * @param referenceUuid uuid of the composite or group where the 'Modification reference' is located + * @param userId id of the user who caused the unreferencing + * @param sharedElementUuid uuid of the referenced shared element in the directory-server + */ + public void removeReference(UUID referenceUuid, String userId, UUID sharedElementUuid) { + Objects.requireNonNull(referenceUuid); + Objects.requireNonNull(sharedElementUuid); + + var path = UriComponentsBuilder.fromPath( + DELIMITER + DIRECTORY_API_VERSION + DELIMITER + "elements/{elementUuid}/references/{referenceUuid}") + .buildAndExpand(sharedElementUuid, referenceUuid) + .toUriString(); + + HttpHeaders headers = new HttpHeaders(); + headers.set(HEADER_USER_ID, userId); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity requestEntity = new HttpEntity<>(headers); + restTemplate.exchange(getDirectoryServerServerBaseUri() + path, HttpMethod.DELETE, requestEntity, ElementAttributes.class); + } + public void createElement(UUID directoryUuid, String description, UUID elementUuid, String elementName, String type, String userId) { UriComponentsBuilder pathBuilder = UriComponentsBuilder.fromPath(DELIMITER + DIRECTORY_API_VERSION + "/directories/{directoryUuid}/elements"); ElementAttributes elementAttributes = new ElementAttributes(elementUuid, elementName, type, userId, 0, description); String path = pathBuilder.buildAndExpand(directoryUuid).toUriString(); HttpHeaders headers = new HttpHeaders(); - headers.set("userId", userId); + headers.set(HEADER_USER_ID, userId); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity requestEntity = new HttpEntity<>(elementAttributes, headers); diff --git a/src/main/java/org/gridsuite/study/server/service/NetworkModificationService.java b/src/main/java/org/gridsuite/study/server/service/NetworkModificationService.java index 291f5402d..731a64ab4 100644 --- a/src/main/java/org/gridsuite/study/server/service/NetworkModificationService.java +++ b/src/main/java/org/gridsuite/study/server/service/NetworkModificationService.java @@ -13,6 +13,7 @@ import org.gridsuite.study.server.StudyConstants; import org.gridsuite.study.server.dto.BuildInfos; import org.gridsuite.study.server.dto.NodeReceiver; +import org.gridsuite.study.server.dto.modification.CompositesToBeInserted; import org.gridsuite.study.server.dto.modification.ModificationApplicationContext; import org.gridsuite.study.server.dto.modification.NetworkModificationMetadata; import org.gridsuite.study.server.dto.modification.NetworkModificationsResult; @@ -68,7 +69,7 @@ public class NetworkModificationService { } public void setNetworkModificationServerBaseUri(String networkModificationServerBaseUri) { - this.networkModificationServerBaseUri = networkModificationServerBaseUri + DELIMITER; + this.networkModificationServerBaseUri = networkModificationServerBaseUri; } private String getNetworkModificationServerURI(boolean addNetworksPart) { @@ -223,6 +224,54 @@ public void restoreModifications(UUID groupUUid, List modificationsUuids) restTemplate.exchange(path, HttpMethod.PUT, httpEntity, Void.class); } + /** + * @return references data of the modificationsUuids as Pair of : + * - element uuid in directory server + * - uuid of its mother composite (null if the modification is at the root level) + */ + public Map getReferencesData(List modificationsUuids) { + Objects.requireNonNull(modificationsUuids); + var path = UriComponentsBuilder + .fromUriString(getNetworkModificationServerURI(false) + "references") + .queryParam(UUIDS, modificationsUuids) + .buildAndExpand() + .toUriString(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity> httpEntity = new HttpEntity<>(headers); + + return restTemplate.exchange( + path, + HttpMethod.GET, + httpEntity, + new ParameterizedTypeReference>() { } + ).getBody(); + } + + /** + * @return references data of the modifications in the group as Pairof : + * - element uuid in directory server + * - uuid of its mother composite (null if the modification is at the root level) + */ + public Map getReferencesDataFromGroup(UUID groupUuid) { + Objects.requireNonNull(groupUuid); + var path = UriComponentsBuilder.fromPath(GROUP_PATH + DELIMITER + "references"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity> httpEntity = new HttpEntity<>(headers); + + return restTemplate.exchange( + getNetworkModificationServerURI(false) + path.buildAndExpand(groupUuid).toUriString(), + HttpMethod.GET, + httpEntity, + new ParameterizedTypeReference>() { } + ).getBody(); + } + public void buildNode(@NonNull UUID nodeUuid, @NonNull UUID rootNetworkUuid, @NonNull BuildInfos buildInfos, AbstractWorkflowInfos workflowInfos) { UUID networkUuid = rootNetworkService.getNetworkUuid(rootNetworkUuid); String receiver = buildReceiver(nodeUuid, rootNetworkUuid); @@ -285,13 +334,13 @@ public NetworkModificationsResult duplicateModifications(UUID groupUuid, public NetworkModificationsResult insertCompositeModifications(UUID groupUuid, CompositeModificationsActionType action, - Pair>, List> modificationContextInfos) { + Pair, List> modificationContextInfos) { var path = UriComponentsBuilder.fromPath(COMPOSITE_PATH + GROUP_PATH) .queryParam(QUERY_PARAM_ACTION, action.name()); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); - HttpEntity>, List>> httpEntity = new HttpEntity<>(modificationContextInfos, headers); + HttpEntity, List>> httpEntity = new HttpEntity<>(modificationContextInfos, headers); return restTemplate.exchange( getNetworkModificationServerURI(false) + path.buildAndExpand(groupUuid).toUriString(), diff --git a/src/main/java/org/gridsuite/study/server/service/StudyService.java b/src/main/java/org/gridsuite/study/server/service/StudyService.java index d838d07c6..ea87b8820 100644 --- a/src/main/java/org/gridsuite/study/server/service/StudyService.java +++ b/src/main/java/org/gridsuite/study/server/service/StudyService.java @@ -82,6 +82,7 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; import static org.gridsuite.study.server.StudyConstants.BUS_ID_TO_ICC_VALUES; @@ -298,7 +299,7 @@ public BasicStudyInfos createStudy(UUID caseUuid, String userId, UUID studyUuid, persistNetwork(rootNetworkInfos, basicStudyInfos.getId(), NetworkModificationTreeService.FIRST_VARIANT_ID, userId, importParameters, CaseImportAction.STUDY_CREATION); } catch (Exception e) { - self.deleteStudyIfNotCreationInProgress(basicStudyInfos.getId()); + self.deleteStudyIfNotCreationInProgress(basicStudyInfos.getId(), userId); throw e; } @@ -492,7 +493,7 @@ public void duplicateStudyAsync(BasicStudyInfos basicStudyInfos, UUID sourceStud } catch (Exception e) { LOGGER.error(e.toString(), e); } finally { - self.deleteStudyIfNotCreationInProgress(basicStudyInfos.getId()); + self.deleteStudyIfNotCreationInProgress(basicStudyInfos.getId(), userId); LOGGER.trace("Create study '{}' from source {} : {} seconds", basicStudyInfos.getId(), sourceStudyUuid, TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startTime.get())); } @@ -548,9 +549,11 @@ private Optional doDeleteStudyIfNotCreationInProgress(UUID stu DeleteStudyInfos deleteStudyInfos = null; if (studyCreationRequestEntity.isEmpty() && studyEntity.isPresent()) { List rootNetworkInfos = getStudyRootNetworksInfos(studyUuid); - // get all modification groups related to the study - List modificationGroupUuids = networkModificationTreeService.getAllStudyNetworkModificationNodeInfo(studyUuid).stream() - .map(NetworkModificationNodeInfoEntity::getModificationGroupUuid).toList(); + // get all modification groups and nodes related to the study + List allStudyNetworkModificationNodeInfo = networkModificationTreeService.getAllStudyNetworkModificationNodeInfo(studyUuid); + List> modificationGroupUuidsNodeUuids = allStudyNetworkModificationNodeInfo.stream() + .map(nodeInfoEntity -> Pair.of(nodeInfoEntity.getModificationGroupUuid(), nodeInfoEntity.getIdNode())) + .toList(); StudyEntity s = studyEntity.get(); networkModificationTreeService.doDeleteTree(studyUuid); studyRepository.deleteById(studyUuid); @@ -560,7 +563,7 @@ private Optional doDeleteStudyIfNotCreationInProgress(UUID stu removeSpreadsheetConfigCollection(s.getSpreadsheetConfigCollectionUuid()); removeWorkspacesConfig(s.getWorkspacesConfigUuid()); removeNadConfigs(s.getNadConfigsUuids().stream().toList()); - deleteStudyInfos = new DeleteStudyInfos(rootNetworkInfos, modificationGroupUuids); + deleteStudyInfos = new DeleteStudyInfos(rootNetworkInfos, modificationGroupUuidsNodeUuids); } else { studyCreationRequestEntity.ifPresent(creationRequestEntity -> studyCreationRequestRepository.deleteById(creationRequestEntity.getId())); } @@ -583,7 +586,7 @@ private void removeNetworkVisualizationParameters(@Nullable UUID uuid) { } @Transactional - public void deleteStudyIfNotCreationInProgress(UUID studyUuid) { + public void deleteStudyIfNotCreationInProgress(UUID studyUuid, String userId) { AtomicReference startTime = new AtomicReference<>(null); Optional deleteStudyInfosOpt = doDeleteStudyIfNotCreationInProgress(studyUuid); if (deleteStudyInfosOpt.isPresent()) { @@ -594,13 +597,25 @@ public void deleteStudyIfNotCreationInProgress(UUID studyUuid) { rootNetworkService.invalidateRootNetworkRemoteInfos(deleteStudyInfos.getRootNetworkInfosList(), false, true); // delete all distant resources linked to nodes - studyServerExecutionService.runAsync(() -> deleteStudyInfos.getModificationGroupUuids().stream().filter(Objects::nonNull).forEach(networkModificationService::deleteModifications)); + studyServerExecutionService.runAsync(() -> deleteStudyInfos.getModificationGroupUuidsNodeUuids().stream() + .filter(Objects::nonNull) + .forEach(groupUuidNodeUuid -> deleteModificationsFromGroup(groupUuidNodeUuid, userId))); LOGGER.trace("Delete study '{}' : {} seconds", studyUuid, TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startTime.get())); } } + private void deleteModificationsFromGroup(Pair groupUuidNodeUuid, String userId) { + // fetch the references data in order to remove those references from directory-server + Map referenceToBeDeleted = networkModificationService.getReferencesDataFromGroup(groupUuidNodeUuid.getFirst()); + referenceToBeDeleted.forEach((modUuid, refUuid) -> + directoryService.removeReference(refUuid != null ? refUuid : groupUuidNodeUuid.getSecond(), userId, modUuid) + ); + + networkModificationService.deleteModifications(groupUuidNodeUuid.getFirst()); + } + @Transactional public CreatedStudyBasicInfos insertStudy(UUID studyUuid, String userId, NetworkInfos networkInfos, CaseInfos caseInfos, ComputationParameterUUIDs computationParameterUUIDs, UUID networkVisualizationParametersUuid, @@ -2000,7 +2015,13 @@ public void deleteNetworkModifications(UUID studyUuid, UUID nodeUuid, List throw new StudyException(NOT_ALLOWED); } UUID groupId = networkModificationTreeService.getModificationGroupUuid(nodeUuid); + + Map referenceToBeDeleted = networkModificationService.getReferencesData(modificationsUuids); networkModificationService.deleteModifications(groupId, modificationsUuids); + // if there are unstashed references modifications in the deleted netmods, those references have to be removed from directory server + referenceToBeDeleted.forEach((modUuid, refUuid) -> + directoryService.removeReference(refUuid != null ? refUuid : nodeUuid, userId, modUuid) + ); // for each root network, remove modifications from excluded ones studyEntity.getRootNetworks().forEach(rootNetworkEntity -> rootNetworkNodeInfoService.updateModificationsToExclude(nodeUuid, rootNetworkEntity.getId(), new HashSet<>(modificationsUuids), true)); @@ -2019,7 +2040,13 @@ public void stashNetworkModifications(UUID studyUuid, UUID nodeUuid, List throw new StudyException(NOT_ALLOWED); } UUID groupId = networkModificationTreeService.getModificationGroupUuid(nodeUuid); + + Map referenceToBeDeleted = networkModificationService.getReferencesData(modificationsUuids); networkModificationService.stashModifications(groupId, modificationsUuids); + // if there are references modifications in the stashed netmods, those references have to be removed from directory server + referenceToBeDeleted.forEach((modUuid, refUuid) -> + directoryService.removeReference(refUuid != null ? refUuid : nodeUuid, userId, modUuid) + ); invalidateNodeTree(studyUuid, nodeUuid); } finally { notificationService.emitEndModificationEquipmentNotification(studyUuid, nodeUuid, childrenUuids); @@ -2120,7 +2147,7 @@ public void deleteNodes(UUID studyUuid, List nodeIds, boolean deleteChildr } } - deleteNodesInfos(deleteNodeInfos); + deleteNodesInfos(deleteNodeInfos, userId); notificationService.emitElementUpdated(studyUuid, userId); } @@ -2147,10 +2174,17 @@ private CompletableFuture deleteInvalidationInfos(InvalidateNodeInfos inva return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)); } - private void deleteNodesInfos(DeleteNodeInfos deleteNodeInfos) { + private void deleteNodesInfos(DeleteNodeInfos deleteNodeInfos, String userId) { List> futures = new ArrayList<>(); futures.add(studyServerExecutionService.runAsync(() -> deleteNodeInfos.getVariantIds().forEach(networkStoreService::deleteVariants))); - futures.add(studyServerExecutionService.runAsync(() -> deleteNodeInfos.getModificationGroupUuids().forEach(networkModificationService::deleteModifications))); + List modificationGroupUuids = deleteNodeInfos.getModificationGroupUuids(); + List removedNodeUuids = deleteNodeInfos.getRemovedNodeUuids(); + List> modificationGroupUuidsNodeUuids = IntStream.range(0, modificationGroupUuids.size()) + .mapToObj(index -> Pair.of(modificationGroupUuids.get(index), removedNodeUuids.get(index))) + .toList(); + futures.add(studyServerExecutionService.runAsync(() -> modificationGroupUuidsNodeUuids.forEach( + groupUuidNodeUuid -> deleteModificationsFromGroup(groupUuidNodeUuid, userId)) + )); futures.add(studyServerExecutionService.runAsync(() -> deleteNodeInfos.getRemovedNodeUuids().forEach(dynamicSimulationEventService::deleteEventsByNodeId))); futures.addAll(rootNetworkNodeInfoService.getRemoteDeletions(deleteNodeInfos)); // Do not wait completion and do not throw exception @@ -2178,8 +2212,15 @@ public void stashNode(UUID studyUuid, UUID nodeId, boolean stashChildren, String ); } + UUID groupId = networkModificationTreeService.getModificationGroupUuid(nodeId); networkModificationTreeService.doStashNode(nodeId, stashChildren); + Map referenceToBeDeleted = networkModificationService.getReferencesDataFromGroup(groupId); + // if there are references modifications in the stashed node, those references have to be removed from directory server + referenceToBeDeleted.forEach((modUuid, refUuid) -> + directoryService.removeReference(refUuid != null ? refUuid : nodeId, userId, modUuid) + ); + if (startTime.get() != null) { LOGGER.trace("Delete node '{}' of study '{}' : {} seconds", nodeId, studyUuid, TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startTime.get())); @@ -2367,13 +2408,25 @@ public UUID assembleModificationsIntoComposite( public void insertCompositeNetworkModifications( UUID targetStudyUuid, UUID targetNodeUuid, - List> compositesInfos, + List compositesInfos, String userId, StudyConstants.CompositeModificationsActionType action) { - duplicateModificationsOrInsertComposites(targetStudyUuid, targetNodeUuid, - (groupUuid, modificationApplicationContexts) -> - networkModificationService.insertCompositeModifications(groupUuid, action, Pair.of(compositesInfos, modificationApplicationContexts)), - userId); + // is some of the inserted modifications are shared, references have to be created in directory server + List sharedCompositeUuids = compositesInfos.stream() + .filter(CompositesToBeInserted::isShared) + .map(CompositesToBeInserted::id) + .toList(); + if (action == StudyConstants.CompositeModificationsActionType.INSERT && !sharedCompositeUuids.isEmpty()) { + directoryService.createsReferencesToSharedComposites(sharedCompositeUuids, userId, targetNodeUuid); + } + + duplicateModificationsOrInsertComposites( + targetStudyUuid, + targetNodeUuid, + (groupUuid, modificationApplicationContexts) -> + networkModificationService.insertCompositeModifications(groupUuid, action, Pair.of(compositesInfos, modificationApplicationContexts)), + userId + ); } private void duplicateModificationsOrInsertComposites( diff --git a/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java b/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java index 823d25a96..d069895b4 100644 --- a/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java +++ b/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java @@ -79,6 +79,7 @@ import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.gridsuite.study.server.StudyConstants.HEADER_ERROR_MESSAGE; import static org.gridsuite.study.server.StudyConstants.QUERY_PARAM_RECEIVER; +import static org.gridsuite.study.server.dto.ReferenceAttributes.ReferenceType.STUDY_NODE; import static org.gridsuite.study.server.error.StudyBusinessErrorCode.MAX_NODE_BUILDS_EXCEEDED; import static org.gridsuite.study.server.error.StudyBusinessErrorCode.NOT_FOUND; import static org.gridsuite.study.server.utils.ImpactUtils.createModificationResultWithElementImpact; @@ -194,6 +195,9 @@ class NetworkModificationTest { @Autowired private NetworkModificationService networkModificationService; + @Autowired + private DirectoryService directoryService; + @Autowired private ReportService reportService; @@ -303,6 +307,7 @@ void setup() { doReturn(baseUrl).when(dynamicMarginCalculationClient).getBaseUri(); networkModificationService.setNetworkModificationServerBaseUri(baseUrl); + directoryService.setDirectoryServerServerBaseUri(baseUrl); userAdminService.setUserAdminServerBaseUri(baseUrl); buildOkStubId = wireMockServer.stubFor(WireMock.post(WireMock.urlPathEqualTo("/v1/networks/" + NETWORK_UUID_STRING + "/build")) @@ -1157,6 +1162,21 @@ void testCreateTwoWindingsTransformer() throws Exception { void deleteModificationRequest() throws Exception { String userId = "userId"; + UUID modificationUuid = UUID.randomUUID(); + // stubs the checks and updates of referenced modifications + Map stubbedReferences = new HashMap<>(); + stubbedReferences.put(modificationUuid, null); + UUID referencesStubId = wireMockServer.stubFor(WireMock.get(WireMock.urlPathEqualTo("/v1/references")) + .withQueryParam("uuids", WireMock.equalTo(modificationUuid.toString())) + .willReturn(WireMock.ok() + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .withBody(mapper.writeValueAsString(stubbedReferences))) + ).getId(); + UUID removeReferencesStubId = wireMockServer.stubFor(WireMock.delete(WireMock.urlPathMatching( + "/v1/elements/" + modificationUuid + "/references/.*")) + .willReturn(WireMock.ok()) + ).getId(); + UUID stubId = wireMockServer.stubFor(WireMock.delete(WireMock.urlPathMatching("/v1/network-modifications")).willReturn(WireMock.ok())).getId(); StudyEntity studyEntity = insertDummyStudy(UUID.fromString(NETWORK_UUID_STRING), CASE_UUID, "UCTE"); @@ -1178,12 +1198,19 @@ void deleteModificationRequest() throws Exception { .header(USER_ID_HEADER, userId)) .andExpect(status().isNotFound()); - UUID modificationUuid = UUID.randomUUID(); mockMvc.perform(delete(URI_NETWORK_MODIF, studyUuid, modificationNode.getId()) .queryParam("uuids", modificationUuid.toString()) .header(USER_ID_HEADER, userId)) .andExpect(status().isOk()); + WireMockUtils.verifyGetRequest(wireMockServer, referencesStubId, "/v1/references", Map.of("uuids", WireMock.equalTo(modificationUuid.toString()))); WireMockUtils.verifyDeleteRequest(wireMockServer, stubId, "/v1/network-modifications", false, Map.of("uuids", WireMock.equalTo(modificationUuid.toString()))); + WireMockUtils.verifyDeleteRequest( + wireMockServer, + removeReferencesStubId, + "/v1/elements/" + modificationUuid + "/references/.*", + true, + Map.of() + ); checkEquipmentDeletingMessagesReceived(studyUuid, modificationNode.getId()); checkEquipmentDeletingFinishedMessagesReceived(studyUuid, modificationNode.getId()); @@ -1194,6 +1221,7 @@ void deleteModificationRequest() throws Exception { .queryParam("uuids", modificationUuid.toString()) .header(USER_ID_HEADER, "userId")) .andExpect(status().isInternalServerError()); + WireMockUtils.verifyGetRequest(wireMockServer, referencesStubId, "/v1/references", Map.of("uuids", WireMock.equalTo(modificationUuid.toString()))); WireMockUtils.verifyDeleteRequest(wireMockServer, stubId, "/v1/network-modifications", false, Map.of("uuids", WireMock.equalTo(modificationUuid.toString()))); checkEquipmentDeletingMessagesReceived(studyUuid, modificationNode.getId()); checkEquipmentDeletingFinishedMessagesReceived(studyUuid, modificationNode.getId()); @@ -2164,8 +2192,9 @@ void testInsertComposite() throws Exception { NetworkModificationNode node1 = createNetworkModificationNode(studyUuid, rootNodeUuid, UUID.randomUUID(), VARIANT_ID, "New node 1", "userId"); UUID nodeUuid1 = node1.getId(); - Pair modification1 = Pair.of(UUID.randomUUID(), "composite 1"); - Pair modification2 = Pair.of(UUID.randomUUID(), "composite 2"); + CompositesToBeInserted modification1 = new CompositesToBeInserted(UUID.randomUUID(), "composite 1", false); + UUID sharedNetModId = UUID.randomUUID(); + CompositesToBeInserted modification2 = new CompositesToBeInserted(sharedNetModId, "composite 2", true); String compositesData = mapper.writeValueAsString( Arrays.asList( modification1, @@ -2177,7 +2206,14 @@ void testInsertComposite() throws Exception { .withQueryParam("action", WireMock.equalTo("INSERT")) .willReturn(WireMock.ok() .withBody(mapper.writeValueAsString(new NetworkModificationsResult(List.of(UUID.randomUUID(), UUID.randomUUID()), List.of(Optional.empty())))) - .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE))).getId(); + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE))); + + // stubs the references creation : + wireMockServer.stubFor(WireMock.post(WireMock.urlPathEqualTo("/v1/elements/" + sharedNetModId + "/references")) + .withHeader(USER_ID_HEADER, WireMock.equalTo(userId)) + .withHeader(HttpHeaders.CONTENT_TYPE, WireMock.containing(MediaType.APPLICATION_JSON_VALUE)) + .willReturn(WireMock.ok() + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE))); // insert 2 composite modifications in node1 mockMvc.perform(put("/v1/studies/{studyUuid}/nodes/{nodeUuid}/composite-modifications?action=INSERT", @@ -2191,7 +2227,7 @@ void testInsertComposite() throws Exception { checkEquipmentUpdatingFinishedMessagesReceived(studyUuid, nodeUuid1); checkElementUpdatedMessageSent(studyUuid, userId); - Pair>, List> modificationBody = + Pair, List> modificationBody = Pair.of( List.of(modification1, modification2), List.of(rootNetworkNodeInfoService.getNetworkModificationApplicationContext(firstRootNetworkUuid, node1.getId(), NETWORK_UUID) @@ -2202,6 +2238,11 @@ void testInsertComposite() throws Exception { WireMockUtilsCriteria.verifyPutRequest(wireMockServer, url, false, Map.of( "action", WireMock.equalTo("INSERT")), expectedBody); + WireMockUtilsCriteria.verifyPostRequest( + wireMockServer, + "/v1/elements/" + sharedNetModId + "/references", + Map.of(), + mapper.writeValueAsString(new ReferenceAttributes(nodeUuid1, STUDY_NODE))); } @Test diff --git a/src/test/java/org/gridsuite/study/server/NetworkModificationTreeTest.java b/src/test/java/org/gridsuite/study/server/NetworkModificationTreeTest.java index 2baf8ed8c..704654bf8 100644 --- a/src/test/java/org/gridsuite/study/server/NetworkModificationTreeTest.java +++ b/src/test/java/org/gridsuite/study/server/NetworkModificationTreeTest.java @@ -287,6 +287,18 @@ public MockResponse dispatch(RecordedRequest request) { if (path.matches("/v1/results.*") && request.getMethod().equals("DELETE")) { return new MockResponse(HttpStatus.OK.value()); + } else if (path.matches("/v1/references\\?uuids=.*") && request.getMethod().equals("GET")) { + return new MockResponse( + HttpStatus.OK.value(), + Headers.of(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE), + "{}" + ); + } else if (path.matches("/v1/groups/.*/references") && Objects.equals(request.getMethod(), "GET")) { + return new MockResponse( + HttpStatus.OK.value(), + Headers.of(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE), + "{}" + ); } else if (path.matches("/v1/network-modifications.*")) { return new MockResponse(HttpStatus.OK.value()); } else if (path.matches("/v1/groups/" + MODIFICATION_GROUP_UUID + "/network-modifications-count.*") && request.getMethod().equals("GET")) { diff --git a/src/test/java/org/gridsuite/study/server/studycontroller/StudyTest.java b/src/test/java/org/gridsuite/study/server/studycontroller/StudyTest.java index f4cfb4e94..282d42967 100644 --- a/src/test/java/org/gridsuite/study/server/studycontroller/StudyTest.java +++ b/src/test/java/org/gridsuite/study/server/studycontroller/StudyTest.java @@ -242,12 +242,14 @@ void testDeleteStudy() throws Exception { UUID stubUuid = wireMockStubs.stubNetworkModificationDeleteGroup(); UUID stubDeleteCaseId = wireMockStubs.caseServer.stubDeleteCase(CASE_UUID_STRING); DeleteStudyStubs deleteStudyStubs = setupDeleteStudyStubs(); + UUID stubReferencesUuid = wireMockStubs.stubGetAllReferencesDataFromGroup(); mockMvc.perform(delete("/v1/studies/{studyUuid}", studyUuid).header(USER_ID_HEADER, "userId")) .andExpect(status().isOk()); assertTrue(studyRepository.findById(studyUuid).isEmpty()); + wireMockStubs.verifyGetReferencesDataFromGroup(stubReferencesUuid); wireMockStubs.verifyNetworkModificationDeleteGroup(stubUuid, false); wireMockStubs.caseServer.verifyDeleteCase(stubDeleteCaseId, CASE_UUID_STRING); deleteStudyStubs.verify(wireMockStubs, computationServerStubs, 10); // voltageInit, loadFlow, securityAnalysis, sensitivityAnalysis, stateEstimation, pccMin, dynamic, shortCircuit @@ -276,11 +278,13 @@ void testDeleteStudyWithError(final CapturedOutput capturedOutput) throws Except }).when(caseService).deleteCase(any()); UUID stubUuid = wireMockStubs.stubNetworkModificationDeleteGroup(); DeleteStudyStubs deleteStudyStubs = setupDeleteStudyStubs(); + UUID stubReferencesUuid = wireMockStubs.stubGetAllReferencesDataFromGroup(); mockMvc.perform(delete("/v1/studies/{studyUuid}", studyUuid).header(USER_ID_HEADER, "userId")) .andExpectAll(status().isOk()); assertTrue(capturedOutput.getOut().contains(StudyServerExecutionService.class.getName() + " - " + CompletionException.class.getName() + ": " + InterruptedException.class.getName())); + wireMockStubs.verifyGetReferencesDataFromGroup(stubReferencesUuid); wireMockStubs.verifyNetworkModificationDeleteGroup(stubUuid, false); deleteStudyStubs.verify(wireMockStubs, computationServerStubs, 3); // loadflow, security, sensitivity, stateEstimation, shortCircuit, pccMin } @@ -291,6 +295,7 @@ void testDeleteStudyWithNonExistingCase() throws Exception { UUID nonExistingCaseUuid = UUID.randomUUID(); UUID stubUuid = wireMockStubs.stubNetworkModificationDeleteGroup(); + UUID stubReferencesUuid = wireMockStubs.stubGetAllReferencesDataFromGroup(); // Changing the study case uuid with a non-existing case StudyEntity studyEntity = studyRepository.findById(studyUuid).orElse(null); @@ -307,6 +312,7 @@ void testDeleteStudyWithNonExistingCase() throws Exception { assertTrue(studyRepository.findById(studyUuid).isEmpty()); + wireMockStubs.verifyGetReferencesDataFromGroup(stubReferencesUuid); wireMockStubs.verifyNetworkModificationDeleteGroup(stubUuid, false); wireMockStubs.caseServer.verifyDeleteCase(stubDeleteCaseId, nonExistingCaseUuid.toString()); deleteStudyStubs.verify(wireMockStubs, computationServerStubs, 10); diff --git a/src/test/java/org/gridsuite/study/server/utils/wiremock/WireMockStubs.java b/src/test/java/org/gridsuite/study/server/utils/wiremock/WireMockStubs.java index d4c61c427..36afd094e 100644 --- a/src/test/java/org/gridsuite/study/server/utils/wiremock/WireMockStubs.java +++ b/src/test/java/org/gridsuite/study/server/utils/wiremock/WireMockStubs.java @@ -238,6 +238,17 @@ public UUID stubNetworkModificationDeleteGroup() { ).getId(); } + public UUID stubGetAllReferencesDataFromGroup() { + return wireMock.stubFor(WireMock.get(WireMock.urlPathMatching(URI_NETWORK_MODIFICATION_GROUPS + DELIMITER + ".*/references")) + .willReturn(WireMock.ok() + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .withBody("{}"))).getId(); + } + + public void verifyGetReferencesDataFromGroup(UUID getReferencesDataUuid) { + verifyGetRequest(wireMock, getReferencesDataUuid, URI_NETWORK_MODIFICATION_GROUPS + DELIMITER + ".*/references", true, Map.of(), 1); + } + public UUID stubNetworkModificationDeleteIndex() { return wireMock.stubFor(WireMock.delete(WireMock.urlPathMatching(URI_NETWORK_MODIFICATION + DELIMITER + "index.*")) .willReturn(WireMock.ok())