Skip to content
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
f5553f3
export study
ghazwarhili Aug 7, 2026
c22592d
add TU
ghazwarhili Aug 7, 2026
84ff55c
enhance coverage code
ghazwarhili Aug 7, 2026
aa595a3
rework exportStudyArchive()
etiennehomer Aug 7, 2026
db1e926
code review remarks
ghazwarhili Aug 7, 2026
e04ee14
remove BuildStatus
ghazwarhili Aug 7, 2026
34e39f6
update TU
ghazwarhili Aug 7, 2026
7b241aa
add index
ghazwarhili Aug 10, 2026
6b0c1c8
Merge branch 'main' into razwa/export-study
ghazwarhili Aug 10, 2026
2ca0b42
remove CaseExportInfos
ghazwarhili Aug 10, 2026
115841b
Merge branch 'main' into razwa/export-study
ghazwarhili Aug 11, 2026
836cbfd
fix sonar issues
ghazwarhili Aug 11, 2026
83bb25b
code review Etienne L
ghazwarhili Aug 11, 2026
107d377
enhance TU
ghazwarhili Aug 11, 2026
ca94ad3
import study
ghazwarhili Aug 11, 2026
a217703
simplify the studyentity creation
ghazwarhili Aug 12, 2026
4c79e99
add TU
ghazwarhili Aug 12, 2026
43b9d7d
resolve conflicts
ghazwarhili Aug 13, 2026
ae80bc2
revert useless changes
ghazwarhili Aug 13, 2026
53a1e22
revert useless changes
ghazwarhili Aug 13, 2026
cf2cda1
revert useless changes
ghazwarhili Aug 13, 2026
6223d03
add StudyImportService
ghazwarhili Aug 14, 2026
b1fb871
resolve conflicts
ghazwarhili Aug 14, 2026
67b9307
code review rabbit
ghazwarhili Aug 14, 2026
f504693
enhance comments
ghazwarhili Aug 17, 2026
7742935
revert
ghazwarhili Aug 17, 2026
f613fb4
renaming fix
ghazwarhili Aug 17, 2026
05f683e
resolve conflicts
ghazwarhili Aug 28, 2026
9dfe44c
Fix merge
etiennehomer Aug 31, 2026
51776b7
resolve conflicts
ghazwarhili Sep 1, 2026
d3c6cb8
code review remarks
ghazwarhili Sep 4, 2026
df48eb0
revert unused changes
ghazwarhili Sep 4, 2026
52c67f8
Merge branch 'main' into razwa/import-study
ghazwarhili Sep 4, 2026
15797be
code review remarks
ghazwarhili Sep 4, 2026
d8e6c85
clean code
ghazwarhili Sep 4, 2026
4d85977
add emitStudyCreationFinished
ghazwarhili Sep 4, 2026
61f2dd2
fix test
ghazwarhili Sep 4, 2026
91b55b3
Merge branch 'main' into razwa/import-study
ghazwarhili Sep 7, 2026
14d9d7b
clean code
ghazwarhili Sep 7, 2026
fae45b0
Merge branch 'main' into razwa/import-study
ghazwarhili Sep 7, 2026
a4caf4d
fix merge
ghazwarhili Sep 7, 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,11 +18,13 @@
import org.gridsuite.filter.utils.EquipmentType;
import org.gridsuite.study.server.StudyApi;
import org.gridsuite.study.server.dto.*;
import org.gridsuite.study.server.dto.caseimport.CaseImportAction;
import org.gridsuite.study.server.dto.elasticsearch.EquipmentInfos;
import org.gridsuite.study.server.dto.modification.*;
import org.gridsuite.study.server.dto.networkexport.ExportNetworkStatus;
import org.gridsuite.study.server.dto.networkexport.NodeExportInfos;
import org.gridsuite.study.server.dto.sequence.NodeSequenceType;
import org.gridsuite.study.server.dto.studyexport.TreeExportInfos;
import org.gridsuite.study.server.elasticsearch.EquipmentInfosService;
import org.gridsuite.study.server.error.StudyException;
import org.gridsuite.study.server.exception.PartialResultException;
Expand Down Expand Up @@ -62,6 +64,7 @@ public class StudyController {
private final RootNetworkService rootNetworkService;
private final RebuildNodeService rebuildNodeService;
private final StudyExportService studyExportService;
private final StudyImportService studyImportService;

public StudyController(StudyService studyService,
NetworkService networkStoreService,
Expand All @@ -72,7 +75,8 @@ public StudyController(StudyService studyService,
RemoteServicesInspector remoteServicesInspector,
RootNetworkService rootNetworkService,
RebuildNodeService rebuildNodeService,
StudyExportService studyExportService) {
StudyExportService studyExportService,
StudyImportService studyImportService) {
this.studyService = studyService;
this.networkModificationTreeService = networkModificationTreeService;
this.networkStoreService = networkStoreService;
Expand All @@ -83,6 +87,7 @@ public StudyController(StudyService studyService,
this.rootNetworkService = rootNetworkService;
this.rebuildNodeService = rebuildNodeService;
this.studyExportService = studyExportService;
this.studyImportService = studyImportService;
}

@InitBinder
Expand Down Expand Up @@ -175,7 +180,8 @@ public ResponseEntity<List<BasicRootNetworkInfos>> getRootNetworks(@PathVariable
public ResponseEntity<RootNetworkRequestInfos> createRootNetwork(@PathVariable("studyUuid") UUID studyUuid,
Comment thread
ghazwarhili marked this conversation as resolved.
@RequestBody RootNetworkInfos rootNetworkInfos,
@RequestHeader(HEADER_USER_ID) String userId) {
return ResponseEntity.ok().body(studyService.createRootNetworkRequest(studyUuid, rootNetworkInfos, userId));
rootNetworkInfos.setId(null);
Comment thread
ghazwarhili marked this conversation as resolved.
Outdated
return ResponseEntity.ok().body(studyService.createRootNetworkRequest(studyUuid, rootNetworkInfos, userId, CaseImportAction.ROOT_NETWORK_CREATION));
}

@PutMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}")
Expand Down Expand Up @@ -1615,4 +1621,13 @@ public ResponseEntity<Resource> exportStudy(@PathVariable("studyUuid") UUID stud
headers.setContentType(MediaType.parseMediaType("application/zip"));
return ResponseEntity.ok().headers(headers).body(studyExportService.exportStudy(studyUuid, userId));
}

@PostMapping(value = "/studies/import")
@Operation(summary = "Create a study and its root networks from a previously exported study archive")
@ApiResponse(responseCode = "200", description = "Study import initiated successfully")
public ResponseEntity<Void> importStudyWithCaseImportAction(@RequestBody TreeExportInfos treeExportInfos,
@RequestHeader(HEADER_USER_ID) String userId) {
studyImportService.importStudyWithCaseImportAction(treeExportInfos, userId);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
public enum CaseImportAction {
STUDY_CREATION("Study creation"),
ROOT_NETWORK_CREATION("Root network creation"),
ROOT_NETWORK_CREATION_FOR_STUDY_IMPORT("Root network creation for study import"),
NETWORK_RECREATION("Network recreation"),
ROOT_NETWORK_MODIFICATION("Root network modification");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.gridsuite.study.server.repository.voltageinit.StudyVoltageInitParametersEntity;

import java.util.*;
import java.util.stream.Collectors;

/**
* @author Abdelsalem Hedhili <abdelsalem.hedhili at rte-france.com>
Expand All @@ -36,6 +37,18 @@ public class StudyEntity extends AbstractManuallyAssignedIdentifierEntity<UUID>
@Builder.Default
private List<RootNetworkEntity> rootNetworks = new ArrayList<>();

/**
* Desired order of root network ids for an in-progress batch import (they can complete out of order).
* Used by {@link #addRootNetwork(RootNetworkEntity)} to place each one correctly; cleared once done.
*/
@ElementCollection
@CollectionTable(name = "StudyRootNetworkOrder", foreignKey = @ForeignKey(
name = "study_root_network_order_fk"
))
@OrderColumn(name = "index")
@Column(name = "rootNetworkUuid")
private List<UUID> rootNetworkOrder;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why that ? you have an index in root_network table

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

not needed anymore


/**
* @deprecated to remove when the data is migrated into the loadflow-server
*/
Expand Down Expand Up @@ -140,7 +153,21 @@ public RootNetworkEntity getFirstRootNetwork() {
public void addRootNetwork(RootNetworkEntity rootNetworkEntity) {
rootNetworkEntity.setStudy(this);
rootNetworkEntity.setIndexationStatus(RootNetworkIndexationStatus.INDEXED);
rootNetworks.add(rootNetworkEntity);
rootNetworks.add(resolveInsertPosition(rootNetworkEntity.getId()), rootNetworkEntity);
}

/**
* Position among the root networks already present, for the given target id: the count of ids that
* should come before it in {@link #rootNetworkOrder} and are already in {@link #rootNetworks}. Falls back
* to appending at the end when there is no pending import batch, or the id isn't part of one.
*/
private int resolveInsertPosition(UUID rootNetworkId) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

insert with same indexes. THis is not needed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

not needed anymore

int targetPos = rootNetworkOrder == null ? -1 : rootNetworkOrder.indexOf(rootNetworkId);
if (targetPos < 0) {
return rootNetworks.size();
}
Set<UUID> alreadyPresent = rootNetworks.stream().map(RootNetworkEntity::getId).collect(Collectors.toSet());
return (int) rootNetworkOrder.subList(0, targetPos).stream().filter(alreadyPresent::contains).count();
}

public void deleteRootNetworks(Set<UUID> uuids) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@

@Repository
public interface StudyRepository extends JpaRepository<StudyEntity, UUID> {
@EntityGraph(attributePaths = {"rootNetworks"}, type = EntityGraph.EntityGraphType.LOAD)
@EntityGraph(attributePaths = {"rootNetworks", "rootNetworkOrder"}, type = EntityGraph.EntityGraphType.LOAD)
Optional<StudyEntity> findWithRootNetworksById(UUID id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/
package org.gridsuite.study.server.repository.rootnetwork;

import org.gridsuite.study.server.dto.RootNetworkAction;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
Expand All @@ -17,6 +18,8 @@ public interface RootNetworkRequestRepository extends JpaRepository<RootNetworkR

int countAllByStudyUuid(UUID studyUuid);

int countAllByStudyUuidAndActionRequest(UUID studyUuid, RootNetworkAction actionRequest);

Optional<RootNetworkRequestRepository> findByNameAndStudyUuid(String name, UUID studyUuid);

Optional<RootNetworkRequestRepository> findByTagAndStudyUuid(String tag, UUID studyUuid);
Expand Down
25 changes: 22 additions & 3 deletions src/main/java/org/gridsuite/study/server/service/CaseService.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Service;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;

import static org.gridsuite.study.server.StudyConstants.CASE_API_VERSION;
Expand Down Expand Up @@ -96,11 +100,26 @@ public UUID createCase(String caseKey, String contentType) {
return restTemplate.exchange(caseServerBaseUri + path, HttpMethod.POST, null, UUID.class).getBody();
}

public ResponseEntity<byte[]> getCaseContent(UUID caseUuid) {
public void streamCaseContent(UUID caseUuid, CaseContentHandler handler) throws IOException {
String path = UriComponentsBuilder.fromPath(DELIMITER + CASE_API_VERSION + "/cases/{caseUuid}")
.buildAndExpand(caseUuid)
.toUriString();

return restTemplate.exchange(caseServerBaseUri + path, HttpMethod.GET, null, byte[].class);
try {
restTemplate.execute(caseServerBaseUri + path, HttpMethod.GET, null, (ClientHttpResponse response) -> {
handler.handle(response.getHeaders().getFirst(HttpHeaders.CONTENT_ENCODING), response.getBody());
return null;
});
} catch (ResourceAccessException e) {
if (e.getCause() instanceof IOException ioException) {
throw ioException;
}
throw e;
}
}

@FunctionalInterface

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

better create a new PR for export changes

And here, why this change ? The handler is needed ?

public interface CaseContentHandler {
void handle(String contentEncoding, InputStream body) throws IOException;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@

private final NotificationService notificationService;
private final StudyService studyService;
private final StudyImportService studyImportService;
private final CaseService caseService;
private final LoadFlowRestService loadFlowRestService;
private final NetworkModificationTreeService networkModificationTreeService;
private final StudyConfigService studyConfigService;
private final RootNetworkNodeInfoService rootNetworkNodeInfoService;
private final DirectoryService directoryService;
private final ComputationParametersService computationParametersService;
Expand All @@ -79,10 +79,10 @@
public ConsumerService(ObjectMapper objectMapper,
NotificationService notificationService,
StudyService studyService,
StudyImportService studyImportService,
CaseService caseService,
LoadFlowRestService loadFlowRestService,
NetworkModificationTreeService networkModificationTreeService,
StudyConfigService studyConfigService,
RootNetworkNodeInfoService rootNetworkNodeInfoService,
DirectoryService directoryService,
ComputationParametersService computationParametersService,
Expand All @@ -91,10 +91,10 @@
this.objectMapper = objectMapper;
this.notificationService = notificationService;
this.studyService = studyService;
this.studyImportService = studyImportService;
this.caseService = caseService;
this.loadFlowRestService = loadFlowRestService;
this.networkModificationTreeService = networkModificationTreeService;
this.studyConfigService = studyConfigService;
this.rootNetworkNodeInfoService = rootNetworkNodeInfoService;
this.directoryService = directoryService;
this.computationParametersService = computationParametersService;
Expand Down Expand Up @@ -250,6 +250,20 @@
.networkInfos(networkInfos)
.importParameters(importParameters)
.build());
case ROOT_NETWORK_CREATION_FOR_STUDY_IMPORT -> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why ROOT_NETWORK_CREATION_FOR_STUDY_IMPORT ?
Should be a STUDY_CREATION I think. To be discussed
I think a simpler solution would have be to only persist the first root network. And then load lazily the other root networks when the user click

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

not needed anymore
removed

try {
studyService.createRootNetwork(studyUuid, RootNetworkInfos.builder()
.id(rootNetworkUuid)
.caseInfos(caseInfos)
.reportUuid(importReportUuid)
.networkInfos(networkInfos)
.importParameters(importParameters)
.build());
} finally {
studyService.deleteRootNetworkRequest(rootNetworkUuid);
studyImportService.checkFinishedStudyImport(studyUuid, userId);
}
}
case NETWORK_RECREATION -> studyService.updateNetwork(studyUuid, rootNetworkUuid, networkInfos, userId);
case ROOT_NETWORK_MODIFICATION -> studyService.modifyRootNetwork(studyUuid, RootNetworkInfos.builder()
.id(rootNetworkUuid)
Expand Down Expand Up @@ -281,73 +295,15 @@
UserProfileInfos userProfileInfos = studyService.getUserProfile(userId);

ComputationParameterUUIDs computationParameterUUIDs = computationParametersService.createDefaultComputationParameters(userId, userProfileInfos);
UUID networkVisualizationParametersUuid = createDefaultNetworkVisualizationParameters(userId, userProfileInfos);
UUID spreadsheetConfigCollectionUuid = createDefaultSpreadsheetConfigCollection(userId, userProfileInfos);
UUID workspacesConfigUuid = createWorkspacesConfig(userProfileInfos);
UUID networkVisualizationParametersUuid = studyService.createDefaultNetworkVisualizationParameters(userId, userProfileInfos);
UUID spreadsheetConfigCollectionUuid = studyService.createDefaultSpreadsheetConfigCollection(userId, userProfileInfos);
UUID workspacesConfigUuid = studyService.createWorkspacesConfig(userProfileInfos);

studyService.insertStudy(studyUuid, userId, networkInfos, caseInfos, computationParameterUUIDs,
networkVisualizationParametersUuid, spreadsheetConfigCollectionUuid, workspacesConfigUuid,
importParameters, importReportUuid);
}

private UUID createDefaultNetworkVisualizationParameters(String userId, UserProfileInfos userProfileInfos) {
if (userProfileInfos != null && userProfileInfos.getNetworkVisualizationParameterId() != null) {
// try to access/duplicate the user profile network visualization parameters
try {
return studyConfigService.duplicateNetworkVisualizationParameters(userProfileInfos.getNetworkVisualizationParameterId());
} catch (Exception e) {
// TODO try to report a log in Root subreporter ?
LOGGER.error(String.format("Could not duplicate network visualization parameters with id '%s' from user/profile '%s/%s'. Using default parameters",
userProfileInfos.getNetworkVisualizationParameterId(), userId, userProfileInfos.getName()), e);
}
}
// no profile, or no/bad network visualization parameters in profile => use default values
try {
return studyConfigService.createDefaultNetworkVisualizationParameters();
} catch (final Exception e) {
LOGGER.error("Error while creating network visualization default parameters", e);
return null;
}
}

private UUID createDefaultSpreadsheetConfigCollection(String userId, UserProfileInfos userProfileInfos) {
if (userProfileInfos != null && userProfileInfos.getSpreadsheetConfigCollectionId() != null) {
// try to access/duplicate the user profile spreadsheet config collection
try {
return studyConfigService.duplicateSpreadsheetConfigCollection(userProfileInfos.getSpreadsheetConfigCollectionId());
} catch (Exception e) {
// TODO try to report a log in Root subreporter ?
LOGGER.error(String.format("Could not duplicate spreadsheet config collection with id '%s' from user/profile '%s/%s'. Using default spreadsheet config collection",
userProfileInfos.getSpreadsheetConfigCollectionId(), userId, userProfileInfos.getName()), e);
}
}
// no profile, or no/bad spreadsheet config collection in profile => use default values
try {
return studyConfigService.createDefaultSpreadsheetConfigCollection();
} catch (final Exception e) {
LOGGER.error("Error while creating default spreadsheet config collection", e);
return null;
}
}

@SuppressWarnings("checkstyle:LambdaBodyLength")
private UUID createWorkspacesConfig(UserProfileInfos userProfileInfos) {
try {
List<UUID> workspaceIds = new ArrayList<>();
if (userProfileInfos != null && userProfileInfos.getWorkspaceId() != null) {
// Create config with profile workspace as first, and two empty workspaces
workspaceIds.add(userProfileInfos.getWorkspaceId());
workspaceIds.add(null);
workspaceIds.add(null);
}
// Empty list will create default config
return studyConfigService.createWorkspacesConfigFromWorkspaces(workspaceIds);
} catch (final Exception e) {
LOGGER.error("Error while creating workspace collection", e);
return null;
}
}

@Bean
@SuppressWarnings("checkstyle:LambdaBodyLength")
public Consumer<Message<String>> consumeCaseImportFailed() {
Expand All @@ -364,14 +320,19 @@
String userId = receiver.getUserId();
UUID rootNetworkUuid = receiver.getRootNetworkUuid();

if (receiver.getCaseImportAction() == CaseImportAction.STUDY_CREATION) {
CaseImportAction caseImportAction = receiver.getCaseImportAction();
if (caseImportAction == CaseImportAction.STUDY_CREATION) {
studyService.deleteStudyIfNotCreationInProgress(studyUuid, userId);
notificationService.emitStudyCreationError(studyUuid, userId, errorMessage);
} else {
if (receiver.getCaseImportAction() == CaseImportAction.ROOT_NETWORK_CREATION) {
if (caseImportAction == CaseImportAction.ROOT_NETWORK_CREATION) {

Check warning on line 328 in src/main/java/org/gridsuite/study/server/service/ConsumerService.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace the chain of if/else with a switch expression.

See more on https://sonarcloud.io/project/issues?id=org.gridsuite%3Astudy-server&issues=AaAA_8Qnnox61m2XXe6F&open=AaAA_8Qnnox61m2XXe6F&pullRequest=1057
studyService.deleteRootNetworkRequest(rootNetworkUuid);
} else if (caseImportAction == CaseImportAction.ROOT_NETWORK_CREATION_FOR_STUDY_IMPORT) {
studyService.deleteRootNetworkRequest(rootNetworkUuid);
studyImportService.checkFinishedStudyImport(studyUuid, userId);
} else {
notificationService.emitRootNetworksUpdateFailed(studyUuid, errorMessage);
}
notificationService.emitRootNetworksUpdateFailed(studyUuid, errorMessage);
}
} catch (Exception e) {
LOGGER.error(e.toString(), e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,10 @@ public List<RootNetworkRequestEntity> geRootNetworkRequests(UUID studyUuid) {
return rootNetworkRequestRepository.findAllByStudyUuid(studyUuid);
}

public int countRootNetworkCreationRequests(UUID studyUuid) {
return rootNetworkRequestRepository.countAllByStudyUuidAndActionRequest(studyUuid, RootNetworkAction.ROOT_NETWORK_CREATION);
}

public void deleteRootNetworkRequest(RootNetworkRequestEntity rootNetworkRequestEntity) {
rootNetworkRequestRepository.delete(rootNetworkRequestEntity);
}
Expand Down
Loading
Loading