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
Original file line number Diff line number Diff line change
Expand Up @@ -1346,13 +1346,11 @@ public ResponseEntity<String> getNetworkModifications(@Parameter(description = "
return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(networkModificationTreeService.getNetworkModifications(nodeUuid, onlyStashed, onlyMetadata));
}

@GetMapping(value = "/studies/{studyUuid}/nodes/{nodeUuid}/network-modifications/export", produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Get network modifications to export for a given node")
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The network modifications to export was returned"), @ApiResponse(responseCode = "404", description = "The study/node is not found")})
public ResponseEntity<String> getExportedNetworkModifications(@Parameter(description = "Study UUID") @PathVariable("studyUuid") UUID studyUuid,
@Parameter(description = "Node UUID") @PathVariable("nodeUuid") UUID nodeUuid) {
studyService.assertIsStudyAndNodeExist(studyUuid, nodeUuid);
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(studyService.getExportedNetworkModifications(studyUuid, nodeUuid));
@GetMapping(value = "/studies/{studyUuid}/network-modifications/export", produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Export a full study: root networks + node tree with embedded modifications")
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The study export"), @ApiResponse(responseCode = "404", description = "The study is not found")})
public ResponseEntity<StudyExportInfos> exportStudy(@Parameter(description = "Study UUID") @PathVariable("studyUuid") UUID studyUuid) {
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(studyService.getStudyExport(studyUuid));
}

@GetMapping(value = "/studies/{studyUuid}/nodes/{nodeUuid}/excluded-network-modifications", produces = MediaType.APPLICATION_JSON_VALUE)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* 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.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.*;

import java.util.Map;
import java.util.UUID;

/**
* @author Rehili Ghazwa <ghazwa.rehili at rte-france.com>
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class RootNetworkExportInfos {
private UUID id;
private String name;
private String description;
private String tag;
private String caseName;
private String caseFormat;
private Map<String, String> importParameters;
}
14 changes: 14 additions & 0 deletions src/main/java/org/gridsuite/study/server/dto/StudyExportInfos.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.gridsuite.study.server.dto;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;

import java.util.List;
import java.util.UUID;

@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public record StudyExportInfos(UUID studyUuid,
List<RootNetworkExportInfos> rootNetworks,
StudyTreeNodeExportInfos rootNode) {
}
20 changes: 20 additions & 0 deletions src/main/java/org/gridsuite/study/server/dto/StudyImportInfos.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.gridsuite.study.server.dto;

import com.fasterxml.jackson.databind.JsonNode;
import lombok.*;

import java.util.List;
import java.util.UUID;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class StudyImportInfos {
private String exportVersion;
private UUID studyUuid;
private String studyName;
private List<RootNetworkExportInfos> rootNetworks;
private JsonNode rootNode;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* 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.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import org.gridsuite.study.server.networkmodificationtree.entities.NetworkModificationNodeType;
import org.gridsuite.study.server.networkmodificationtree.entities.NodeType;

import java.util.List;
import java.util.UUID;

/**
* @author Rehili Ghazwa <ghazwa.rehili at rte-france.com>
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public record StudyTreeNodeExportInfos(UUID uuid,
String name,
NodeType type,
NetworkModificationNodeType nodeType,
@JsonInclude(JsonInclude.Include.NON_EMPTY) Object modifications,
List<StudyTreeNodeExportInfos> nodes) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@

private static final String DELIMITER = "/";
private static final String COMPOSITE_PATH = "network-composite-modifications" + DELIMITER;
private static final String GROUP_PATH = "groups" + DELIMITER + "{groupUuid}";
private static final String GROUPS = "groups";
private static final String GROUP_PATH = GROUPS + DELIMITER + "{groupUuid}";
private static final String NETWORK_MODIFICATIONS_PATH = "network-modifications";
private static final String NETWORK_MODIFICATIONS_COUNT_PATH = "network-modifications-count";
private static final String QUERY_PARAM_ACTION = "action";
Expand Down Expand Up @@ -93,14 +94,18 @@
return restTemplate.exchange(getNetworkModificationServerURI(false) + path, HttpMethod.GET, null, String.class).getBody();
}

public String getModificationsToExport(UUID groupUUid) {
Objects.requireNonNull(groupUUid);
var path = UriComponentsBuilder.fromPath(GROUP_PATH + DELIMITER + NETWORK_MODIFICATIONS_PATH + DELIMITER + "export")
.queryParam(QUERY_PARAM_ERROR_ON_GROUP_NOT_FOUND, false)
.buildAndExpand(groupUUid)
.toUriString();
public Map<UUID, Object> getModificationsInfosToExport(List<UUID> groupUuids) {
Objects.requireNonNull(groupUuids);
if (groupUuids.isEmpty()) {
return Map.of();
}
var path = UriComponentsBuilder.fromPath(GROUPS + DELIMITER + NETWORK_MODIFICATIONS_PATH + DELIMITER + "export")
.queryParam(QUERY_PARAM_ERROR_ON_GROUP_NOT_FOUND, false)
.toUriString();

return restTemplate.exchange(getNetworkModificationServerURI(false) + path, HttpMethod.GET, null, String.class).getBody();
HttpEntity<List<UUID>> httpEntity = new HttpEntity<>(groupUuids);
return restTemplate.exchange(getNetworkModificationServerURI(false) + path, HttpMethod.POST, httpEntity,
new ParameterizedTypeReference<Map<UUID, Object>>() { }).getBody();
}
Comment on lines +97 to 109

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 | 🟠 Major | ⚡ Quick win

Handle null HTTP bodies for batch export responses.

Line 106 and Line 477 can return null when the remote service responds without a body, which can break callers expecting a map. Return Map.of() instead.

💡 Proposed fix
 public Map<UUID, Object> getModificationsInfosToExport(List<UUID> groupUuids) {
     Objects.requireNonNull(groupUuids);
     if (groupUuids.isEmpty()) {
         return Map.of();
@@
-    return restTemplate.exchange(getNetworkModificationServerURI(false) + path, HttpMethod.POST, httpEntity,
-            new ParameterizedTypeReference<Map<UUID, Object>>() { }).getBody();
+    Map<UUID, Object> body = restTemplate.exchange(
+            getNetworkModificationServerURI(false) + path,
+            HttpMethod.POST,
+            httpEntity,
+            new ParameterizedTypeReference<Map<UUID, Object>>() { }
+    ).getBody();
+    return body != null ? body : Map.of();
 }
@@
 public Map<UUID, JsonNode> getModificationsToExportByGroups(List<UUID> groupUuids) {
     Objects.requireNonNull(groupUuids);
     if (groupUuids.isEmpty()) {
         return Map.of();
@@
-    return restTemplate.exchange(
+    Map<UUID, JsonNode> body = restTemplate.exchange(
             getNetworkModificationServerURI(false) + path,
             HttpMethod.POST,
             entity,
             new ParameterizedTypeReference<Map<UUID, JsonNode>>() { }
-    ).getBody();
+    ).getBody();
+    return body != null ? body : Map.of();
 }

Also applies to: 457-478

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/org/gridsuite/study/server/service/NetworkModificationService.java`
around lines 95 - 107, The method getModificationsInfosToExport currently
returns whatever restTemplate.exchange(...).getBody() returns, which can be null
if the remote service responds without a body; change the return path to
defensively handle null by returning Map.of() when getBody() is null. Update
getModificationsInfosToExport (and the other batch-export helper that invokes
restTemplate.exchange(...).getBody() for a Map<UUID,Object>) to capture the
response body into a local variable and return body != null ? body : Map.of(),
ensuring callers always receive a non-null Map.


public Integer getModificationsCount(UUID groupUUid, boolean stashedModifications) {
Expand Down Expand Up @@ -324,7 +329,7 @@
public Map<UUID, UUID> duplicateModificationsGroup(UUID sourceGroupUuid, UUID groupUuid) {
Objects.requireNonNull(groupUuid);
Objects.requireNonNull(sourceGroupUuid);
var path = UriComponentsBuilder.fromPath("groups")

Check failure on line 332 in src/main/java/org/gridsuite/study/server/service/NetworkModificationService.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use already-defined constant 'GROUPS' instead of duplicating its value here.

See more on https://sonarcloud.io/project/issues?id=org.gridsuite%3Astudy-server&issues=AZ5uxSVDT2qWZKvqcsAJ&open=AZ5uxSVDT2qWZKvqcsAJ&pullRequest=1001
.queryParam("duplicateFrom", sourceGroupUuid)
.queryParam("groupUuid", groupUuid)
.buildAndExpand(groupUuid)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@
import com.powsybl.network.store.client.NetworkStoreService;
import com.powsybl.network.store.model.VariantInfos;
import lombok.NonNull;
import org.gridsuite.study.server.dto.*;
import org.gridsuite.study.server.error.StudyException;
import org.gridsuite.study.server.dto.CaseInfos;
import org.gridsuite.study.server.dto.NetworkInfos;
import org.gridsuite.study.server.dto.RootNetworkAction;
import org.gridsuite.study.server.dto.RootNetworkInfos;
import org.gridsuite.study.server.elasticsearch.EquipmentInfosService;
import org.gridsuite.study.server.repository.StudyEntity;
import org.gridsuite.study.server.repository.rootnetwork.RootNetworkRequestEntity;
Expand Down
60 changes: 50 additions & 10 deletions src/main/java/org/gridsuite/study/server/service/StudyService.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,7 @@
import org.gridsuite.study.server.elasticsearch.StudyInfosService;
import org.gridsuite.study.server.error.StudyException;
import org.gridsuite.study.server.networkmodificationtree.dto.*;
import org.gridsuite.study.server.networkmodificationtree.entities.NetworkModificationNodeInfoEntity;
import org.gridsuite.study.server.networkmodificationtree.entities.NodeEntity;
import org.gridsuite.study.server.networkmodificationtree.entities.NodeType;
import org.gridsuite.study.server.networkmodificationtree.entities.RootNetworkNodeInfoEntity;
import org.gridsuite.study.server.networkmodificationtree.entities.*;
import org.gridsuite.study.server.notification.NotificationService;
import org.gridsuite.study.server.notification.dto.NetworkImpactsInfos;
import org.gridsuite.study.server.repository.*;
Expand Down Expand Up @@ -531,14 +528,57 @@ public List<ModificationsSearchResultByNode> searchModifications(@NonNull UUID r
return networkModificationTreeService.getNetworkModificationsByNodeInfos(modificationsByGroup);
}

@Transactional
public String getExportedNetworkModifications(UUID studyUuid, UUID nodeUuid) {
if (!networkModificationTreeService.getStudyUuidForNodeId(nodeUuid).equals(studyUuid)) {
throw new StudyException(NOT_ALLOWED);
@Transactional(readOnly = true)
public StudyExportInfos getStudyExport(UUID studyUuid) {
assertIsStudyExist(studyUuid);

List<RootNetworkExportInfos> rootNetworks = rootNetworkService.getRootNetworkInfosWithLinksInfos(studyUuid).stream()
.map(this::toRootNetworkExportInfos)
.toList();

RootNode rootNode = networkModificationTreeService.getStudyTree(studyUuid, null);

List<UUID> groupUuids = new ArrayList<>();
collectGroupUuids(rootNode, groupUuids);

Map<UUID, Object> modificationsByGroup = networkModificationService.getModificationsInfosToExport(groupUuids);

return new StudyExportInfos(studyUuid, rootNetworks, toNodeExportInfos(rootNode, modificationsByGroup));
}

private RootNetworkExportInfos toRootNetworkExportInfos(RootNetworkInfos infos) {
CaseInfos caseInfos = infos.getCaseInfos();
Map<String, String> importParameters = infos.getImportParameters() != null
? new LinkedHashMap<>(infos.getImportParameters())
: null;
return new RootNetworkExportInfos(
infos.getId(),
infos.getName(),
infos.getDescription(),
infos.getTag(),
caseInfos != null ? caseInfos.getCaseName() : null,
caseInfos != null ? caseInfos.getCaseFormat() : null,
importParameters);
}

private void collectGroupUuids(AbstractNode node, List<UUID> acc) {
if (node instanceof NetworkModificationNode nmNode && nmNode.getModificationGroupUuid() != null) {
acc.add(nmNode.getModificationGroupUuid());
}
UUID groupId = networkModificationTreeService.getModificationGroupUuid(nodeUuid);
node.getChildren().forEach(child -> collectGroupUuids(child, acc));
}

return networkModificationService.getModificationsToExport(groupId);
private StudyTreeNodeExportInfos toNodeExportInfos(AbstractNode node, Map<UUID, Object> modificationsByGroup) {
NetworkModificationNodeType nodeType = null;
Object modifications = null;
if (node instanceof NetworkModificationNode nmNode) {
nodeType = nmNode.getNodeType();
if (modificationsByGroup.get(nmNode.getModificationGroupUuid()) instanceof Map<?, ?> wrapper) {
modifications = wrapper.get("modifications");
}
}
List<StudyTreeNodeExportInfos> children = node.getChildren().stream().map(child -> toNodeExportInfos(child, modificationsByGroup)).toList();
return new StudyTreeNodeExportInfos(node.getId(), node.getName(), node.getType(), nodeType, modifications, children.isEmpty() ? null : children);
}

private Optional<DeleteStudyInfos> doDeleteStudyIfNotCreationInProgress(UUID studyUuid) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import okhttp3.HttpUrl;
import org.gridsuite.study.server.dto.ComputationType;
import org.gridsuite.study.server.dto.RootNetworkNodeInfo;
import org.gridsuite.study.server.dto.StudyExportInfos;
import org.gridsuite.study.server.dto.modification.NetworkModificationResult;
import org.gridsuite.study.server.error.StudyException;
import org.gridsuite.study.server.networkmodificationtree.dto.*;
Expand Down Expand Up @@ -289,6 +290,9 @@ public MockResponse dispatch(RecordedRequest request) {
return new MockResponse(HttpStatus.OK.value());
} else if (path.matches("/v1/network-modifications.*")) {
return new MockResponse(HttpStatus.OK.value());
} else if (path.matches("/v1/groups/network-modifications/export.*") && request.getMethod().equals("POST")) {
return new MockResponse(HttpStatus.OK.value(), Headers.of(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE),
objectMapper.writeValueAsString(Map.of(MODIFICATION_GROUP_UUID, List.of())));
} else if (path.matches("/v1/groups/" + MODIFICATION_GROUP_UUID + "/network-modifications-count.*") && request.getMethod().equals("GET")) {
return new MockResponse(HttpStatus.OK.value(), Headers.of(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE), objectMapper.writeValueAsString(0));
} else if (path.matches("/v1/groups/" + MODIFICATION_GROUP_UUID_2 + "/network-modifications-count.*") && request.getMethod().equals("GET")) {
Expand Down Expand Up @@ -1517,23 +1521,25 @@ void testGetNetworkModificationsNode() throws Exception {
void testGetNetworkModificationsNodeToExport() throws Exception {
String userId = "userId";
RootNode root = createRoot();
NetworkModificationNode node = NetworkModificationTreeTest.buildNetworkModificationConstructionNode("modification node 1", "", UUID.randomUUID(), VARIANT_ID,
NetworkModificationNode node = NetworkModificationTreeTest.buildNetworkModificationConstructionNode(
"modification node 2", "", MODIFICATION_GROUP_UUID, VARIANT_ID,
randomUuidsResultStack(), BuildStatus.BUILT);
createNode(root.getStudyId(), root, node, userId);

mockMvc.perform(get("/v1/studies/{studyUuid}/nodes/{nodeUuid}/network-modifications/export", root.getStudyId(), node.getId()))
.andExpect(status().isNotFound());

// No network modification for a root node
mockMvc.perform(get("/v1/studies/{studyUuid}/nodes/{nodeUuid}/network-modifications/export", root.getStudyId(), root.getId()))
.andExpect(status().isNotFound());

node = NetworkModificationTreeTest.buildNetworkModificationConstructionNode("modification node 2", "", MODIFICATION_GROUP_UUID, VARIANT_ID,
randomUuidsResultStack(), BuildStatus.BUILT);
createNode(root.getStudyId(), root, node, userId);
mockMvc.perform(get("/v1/studies/{studyUuid}/nodes/{nodeUuid}/network-modifications/export", root.getStudyId(), node.getId()))
String result = mockMvc.perform(get("/v1/studies/{studyUuid}/network-modifications/export", root.getStudyId()))
.andExpect(status().isOk())
.andReturn().getResponse().getContentAsString();

StudyExportInfos export = objectMapper.readValue(result, StudyExportInfos.class);
assertEquals(root.getStudyId(), export.studyUuid());
assertEquals("Root", export.rootNode().name());
assertEquals(1, export.rootNode().nodes().size());
}

@Test
void testGetNetworkModificationsToExportStudyNotFound() throws Exception {
mockMvc.perform(get("/v1/studies/{studyUuid}/network-modifications/export", UUID.randomUUID()))
.andExpect(status().isNotFound());
}

private void checkMessagesReceivedWhenRestoreOrStash(RootNode root, NetworkModificationNode node, String restoreOrStashUpdateType) {
Expand Down
Loading