Skip to content
Closed
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 @@ -144,6 +144,19 @@ public ResponseEntity<Map<UUID, UUID>> getParentComposites(
.body(networkModificationService.findModificationParentComposites(networkModificationUuids));
}

/**
* @return modification uuid -> uuid of the top-level group ultimately containing it, walking up through as
* many nested composite modifications as needed; modifications not reachable from any group have no entry
*/
@GetMapping(value = "/root-groups", produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "For each given network modification, find the top-level group ultimately containing it")
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The root groups were returned")})
public ResponseEntity<Map<UUID, UUID>> getRootGroups(
@Parameter(description = "Network modification UUIDs") @RequestParam("uuids") List<UUID> networkModificationUuids) {
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON)
.body(networkModificationService.findModificationRootGroups(networkModificationUuids));
}

@PutMapping(value = "/{uuid}/replace", consumes = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Replaces all the network modifications inside a network composite modification")
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The composite modification has been updated")})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,29 @@ SELECT CAST(m.id AS VARCHAR), CAST(m.container_id AS VARCHAR)
""", nativeQuery = true)
List<Object[]> findCompositeContainerIdsByModificationIds(@Param("uuids") Collection<UUID> uuids);

/**
* Walks up from each given modification, through as many nested composite modifications as needed, to the
* top-level group that ultimately contains it.
* @return one [modification id, root group id] row per given modification uuid that is actually reachable
* from a group; ids not found (deleted, or orphaned from any group) have no row
*/
@Query(value = """
WITH RECURSIVE container_chain(modification_id, current_container_id) AS (
SELECT m.id AS modification_id, m.container_id AS current_container_id
FROM modification m
WHERE m.id IN :uuids
UNION ALL
SELECT cc.modification_id, m2.container_id
FROM container_chain cc
JOIN modification m2 ON m2.id = cc.current_container_id
)
SELECT CAST(cc.modification_id AS VARCHAR), CAST(cc.current_container_id AS VARCHAR)
FROM container_chain cc
JOIN modification_container c ON c.id = cc.current_container_id
WHERE c.type = 'GROUP'
""", nativeQuery = true)
List<Object[]> findRootGroupIdsByModificationIds(@Param("uuids") Collection<UUID> uuids);

@Query("""
SELECT COUNT(m) FROM ModificationEntity m
WHERE m.container.id = :containerId AND m.stashed = :stashed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,14 @@ public Map<UUID, UUID> findModificationParentComposites(@NonNull List<UUID> modi
row -> UUID.fromString((String) row[1])));
}

@Transactional(readOnly = true)
public Map<UUID, UUID> findModificationRootGroups(@NonNull List<UUID> modificationUuids) {
return modificationRepository.findRootGroupIdsByModificationIds(modificationUuids).stream()
.collect(Collectors.toMap(
row -> UUID.fromString((String) row[0]),
row -> UUID.fromString((String) row[1])));
}

@Transactional
public void stashNetworkModifications(UUID groupUuid, @NonNull List<UUID> modificationUuids) {
for (UUID modificationUuid : modificationUuids) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
Expand Down Expand Up @@ -979,6 +980,120 @@ void testCreateCompositeFromSingleCompositeDoesNotWrap() throws Exception {
children.forEach(child -> assertFalse(child instanceof CompositeModificationInfos));
}

@Test
void testGetParentComposites() throws Exception {
// Create 3 root-level modifications and assemble the first 2 into a composite (which stays in the group)
List<ModificationInfos> rootMods = createSomeSwitchModifications(TEST_GROUP_ID, 3);
List<UUID> rootModUuids = rootMods.stream().map(ModificationInfos::getUuid).toList();
UUID directGroupChildUuid = rootModUuids.get(2);

MvcResult mvcResult = mockMvc.perform(post(URI_COMPOSITE_NETWORK_MODIF_BASE + "/")
.content(mapper.writeValueAsString(rootModUuids.subList(0, 2)))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andReturn();
UUID compositeUuid = mapper.readValue(mvcResult.getResponse().getContentAsString(), new TypeReference<>() { });

// the assembled modifications keep their uuids and are now nested inside the composite
List<UUID> nestedChildUuids = mapper.readValue(
mockMvc.perform(get(URI_GET_COMPOSITE_NETWORK_MODIF_CONTENT + "/network-modifications?uuids={id}", compositeUuid))
.andExpect(status().isOk()).andReturn().getResponse().getContentAsString(),
new TypeReference<Map<UUID, List<ModificationInfos>>>() { })
.get(compositeUuid).stream().map(ModificationInfos::getUuid).toList();
assertEquals(2, nestedChildUuids.size());

UUID unknownUuid = UUID.randomUUID();
mvcResult = mockMvc.perform(get(URI_COMPOSITE_NETWORK_MODIF_BASE + "/parent-composites")
.queryParam("uuids", nestedChildUuids.get(0).toString())
.queryParam("uuids", nestedChildUuids.get(1).toString())
.queryParam("uuids", directGroupChildUuid.toString())
.queryParam("uuids", compositeUuid.toString())
.queryParam("uuids", unknownUuid.toString()))
.andExpect(status().isOk()).andReturn();
Map<UUID, UUID> parentComposites = mapper.readValue(mvcResult.getResponse().getContentAsString(), new TypeReference<>() { });

// only the modifications actually nested in a composite have an entry
assertEquals(2, parentComposites.size());
assertEquals(compositeUuid, parentComposites.get(nestedChildUuids.get(0)));
assertEquals(compositeUuid, parentComposites.get(nestedChildUuids.get(1)));
assertFalse(parentComposites.containsKey(directGroupChildUuid));
assertFalse(parentComposites.containsKey(compositeUuid));
assertFalse(parentComposites.containsKey(unknownUuid));
}

@Test
void testGetParentCompositesWithNoMatch() throws Exception {
mockMvc.perform(get(URI_COMPOSITE_NETWORK_MODIF_BASE + "/parent-composites")
.queryParam("uuids", UUID.randomUUID().toString()))
.andExpect(status().isOk())
.andExpect(content().json("{}"));
}

@Test
void testGetRootGroups() throws Exception {
UUID directGroupChildUuid = createSomeSwitchModifications(TEST_GROUP_ID, 1).getFirst().getUuid();

// Build a standalone outer composite nesting an inner composite nesting a leaf, then insert it into the group
List<ModificationInfos> compositeLeafs = createSomeSwitchModifications(TEST_GROUP2_ID, 2);
UUID innerLeafUuid = compositeLeafs.get(0).getUuid();
UUID outerLeafUuid = compositeLeafs.get(1).getUuid();
MvcResult mvcResult = mockMvc.perform(post(URI_COMPOSITE_NETWORK_MODIF_BASE).queryParam("name", "inner composite")
.content(mapper.writeValueAsString(List.of(innerLeafUuid))).contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andReturn();
UUID innerCompositeUuid = mapper.readValue(mvcResult.getResponse().getContentAsString(), new TypeReference<>() { });

mvcResult = mockMvc.perform(post(URI_COMPOSITE_NETWORK_MODIF_BASE).queryParam("name", "outer composite")
.content(mapper.writeValueAsString(List.of(innerCompositeUuid, outerLeafUuid))).contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andReturn();
UUID outerCompositeUuid = mapper.readValue(mvcResult.getResponse().getContentAsString(), new TypeReference<>() { });

runRequestAsync(mockMvc,
put(URI_COMPOSITE_NETWORK_MODIF_BASE + "/groups/{groupUuid}?action=INSERT", TEST_GROUP_ID)
.content(getJsonBodyModificationCompositeToBeInserted(
List.of(new CompositeInfos(outerCompositeUuid, "outer composite", false, "description"))))
.contentType(MediaType.APPLICATION_JSON),
status().isOk());

// INSERT places a copy of the composite in the group; walk down from it to the leaf nested two composites deep
UUID outerCompositeInGroupUuid = networkModificationRepository.getModifications(TEST_GROUP_ID, true, true).stream()
.filter(m -> COMPOSITE_MODIFICATION == m.getType()).map(ModificationInfos::getUuid).findFirst().orElseThrow();
UUID actualInnerCompositeUuid = mapper.readValue(
mockMvc.perform(get(URI_GET_COMPOSITE_NETWORK_MODIF_CONTENT + "/network-modifications?uuids={id}", outerCompositeInGroupUuid))
.andExpect(status().isOk()).andReturn().getResponse().getContentAsString(),
new TypeReference<Map<UUID, List<ModificationInfos>>>() { })
.get(outerCompositeInGroupUuid).stream()
.filter(m -> COMPOSITE_MODIFICATION == m.getType()).map(ModificationInfos::getUuid).findFirst().orElseThrow();
UUID deeplyNestedLeafUuid = mapper.readValue(
mockMvc.perform(get(URI_GET_COMPOSITE_NETWORK_MODIF_CONTENT + "/network-modifications?uuids={id}", actualInnerCompositeUuid))
.andExpect(status().isOk()).andReturn().getResponse().getContentAsString(),
new TypeReference<Map<UUID, List<ModificationInfos>>>() { })
.get(actualInnerCompositeUuid).getFirst().getUuid();

UUID unknownUuid = UUID.randomUUID();
mvcResult = mockMvc.perform(get(URI_COMPOSITE_NETWORK_MODIF_BASE + "/root-groups")
.queryParam("uuids", deeplyNestedLeafUuid.toString())
.queryParam("uuids", directGroupChildUuid.toString())
.queryParam("uuids", outerCompositeInGroupUuid.toString())
.queryParam("uuids", unknownUuid.toString()))
.andExpect(status().isOk()).andReturn();
Map<UUID, UUID> rootGroups = mapper.readValue(mvcResult.getResponse().getContentAsString(), new TypeReference<>() { });

assertEquals(3, rootGroups.size());
// resolved by walking up through both nested composites
assertEquals(TEST_GROUP_ID, rootGroups.get(deeplyNestedLeafUuid));
assertEquals(TEST_GROUP_ID, rootGroups.get(directGroupChildUuid));
assertEquals(TEST_GROUP_ID, rootGroups.get(outerCompositeInGroupUuid));
// a modification not reachable from any group has no entry
assertFalse(rootGroups.containsKey(unknownUuid));
}

@Test
void testGetRootGroupsWithNoMatch() throws Exception {
mockMvc.perform(get(URI_COMPOSITE_NETWORK_MODIF_BASE + "/root-groups")
.queryParam("uuids", UUID.randomUUID().toString()))
.andExpect(status().isOk())
.andExpect(content().json("{}"));
}

private static void assertContiguousOrder(List<ModificationEntity> modifications) {
List<ModificationEntity> sorted = modifications.stream()
.sorted(Comparator.comparingInt(ModificationEntity::getModificationsOrder))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,51 @@ void shouldFindParentCompositeOnlyForModificationsNestedInAComposite() {
assertFalse(parentComposites.containsKey(directGroupChildUuid));
}

@Test
void shouldFindRootGroupForDirectAndNestedModifications() {
UUID groupUuid = UUID.randomUUID();
CompositeModificationInfos compositeModificationInfos = compositeModification(
UUID.randomUUID(),
List.of(dummyModification(UUID.randomUUID()))
);

List<ModificationInfos> saved = networkModificationRepository.saveModifications(
groupUuid, List.of(
ModificationEntity.fromDTO(compositeModificationInfos),
ModificationEntity.fromDTO(dummyModification(UUID.randomUUID()))
));
CompositeModificationInfos savedComposite = (CompositeModificationInfos) saved.get(0);
UUID compositeUuid = savedComposite.getUuid();
UUID nestedChildUuid = savedComposite.getModificationsInfos().get(0).getUuid();
UUID directGroupChildUuid = saved.get(1).getUuid();

Map<UUID, UUID> rootGroups = networkModificationService.findModificationRootGroups(
List.of(nestedChildUuid, directGroupChildUuid, compositeUuid));

assertEquals(3, rootGroups.size());
assertEquals(groupUuid, rootGroups.get(nestedChildUuid));
assertEquals(groupUuid, rootGroups.get(directGroupChildUuid));
assertEquals(groupUuid, rootGroups.get(compositeUuid));
}

@Test
void shouldFindRootGroupThroughNestedComposites() {
UUID groupUuid = UUID.randomUUID();
CompositeModificationInfos innerComposite = compositeModification(UUID.randomUUID(), List.of(dummyModification(UUID.randomUUID())));
CompositeModificationInfos outerComposite = compositeModification(UUID.randomUUID(), List.of((ModificationInfos) innerComposite));

List<ModificationInfos> saved = networkModificationRepository.saveModifications(groupUuid, List.of(ModificationEntity.fromDTO(outerComposite)));
// fromDTO always mints fresh ids, so read the actual persisted ids back rather than assume the DTO's survive
CompositeModificationInfos savedOuterComposite = (CompositeModificationInfos) saved.get(0);
CompositeModificationInfos savedInnerComposite = (CompositeModificationInfos) savedOuterComposite.getModificationsInfos().get(0);
UUID leafUuid = savedInnerComposite.getModificationsInfos().get(0).getUuid();

Map<UUID, UUID> rootGroups = networkModificationService.findModificationRootGroups(List.of(leafUuid));

assertEquals(1, rootGroups.size());
assertEquals(groupUuid, rootGroups.get(leafUuid));
}

private static LoadModificationInfos dummyModification(UUID uuid) {
return LoadModificationInfos.builder()
.equipmentId("dummyEquipmentId")
Expand Down
Loading