Skip to content

Commit 96b7ba4

Browse files
Merge branch 'develop' into namespaceLinkBug
2 parents 71b8c16 + fe322ae commit 96b7ba4

2 files changed

Lines changed: 147 additions & 23 deletions

File tree

sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -164,17 +164,20 @@ private void processNestedEntityComposition(
164164
return;
165165
}
166166

167+
// Get the actual key field names from the entity instead of hardcoding "ID"
168+
List<String> keyElementNames = getKeyElementNames(nestedDraftEntity.get());
169+
167170
Result nestedRecords =
168171
persistenceService.run(
169-
Select.from(nestedDraftEntity.get())
170-
.columns("ID")
171-
.where(e -> e.get("IsActiveEntity").eq(false)));
172+
Select.from(nestedDraftEntity.get()).where(e -> e.get("IsActiveEntity").eq(false)));
172173

173174
for (Row nestedRecord : nestedRecords) {
174-
Object nestedEntityId = nestedRecord.get("ID");
175-
176175
Map<String, Object> nestedEntityKeys = new HashMap<>();
177-
nestedEntityKeys.put("ID", nestedEntityId);
176+
177+
// Populate the key map with all actual key field names and values
178+
for (String keyName : keyElementNames) {
179+
nestedEntityKeys.put(keyName, nestedRecord.get(keyName));
180+
}
178181
nestedEntityKeys.put("IsActiveEntity", false);
179182

180183
for (Map.Entry<String, String> entry : nestedAttachmentMapping.entrySet()) {
@@ -329,7 +332,20 @@ private void createLink(EventContext context) throws IOException {
329332
String upIdKey =
330333
attachmentDraftEntity.isPresent() ? getUpIdKey(attachmentDraftEntity.get()) : "up__ID";
331334
CqnSelect select = (CqnSelect) context.get("cqn");
332-
String upID = fetchUPIDFromCQN(select);
335+
// Get parent entity to extract its key names dynamically
336+
String parentEntityName = null;
337+
ObjectMapper mapper = new ObjectMapper();
338+
JsonNode root = mapper.readTree(select.toString());
339+
JsonNode refArray = root.path("SELECT").path("from").path("ref");
340+
if (refArray.isArray() && refArray.size() >= 2) {
341+
JsonNode parentNode = refArray.get(refArray.size() - 2);
342+
parentEntityName = parentNode.path("id").asText();
343+
}
344+
345+
Optional<CdsEntity> parentEntity =
346+
parentEntityName != null ? cdsModel.findEntity(parentEntityName) : Optional.empty();
347+
348+
String upID = fetchUPIDFromCQN(select, parentEntity.orElse(null));
333349
String filenameInRequest = context.get("name").toString();
334350

335351
Result result =
@@ -430,6 +446,18 @@ private String getUpIdKey(CdsEntity attachmentDraftEntity) {
430446
return upIdKey;
431447
}
432448

449+
/**
450+
* Retrieves the key element names from a CdsEntity. This method extracts the names of all key
451+
* fields defined in the entity, allowing for dynamic key field handling instead of hardcoding
452+
* "ID".
453+
*
454+
* @param entity the CdsEntity to extract key element names from
455+
* @return a list of key element names
456+
*/
457+
private List<String> getKeyElementNames(CdsEntity entity) {
458+
return entity.elements().filter(CdsElement::isKey).map(CdsElement::getName).toList();
459+
}
460+
433461
private void checkAttachmentConstraints(
434462
EventContext context, CdsEntity attachmentDraftEntity, String upID, String upIdKey)
435463
throws ServiceException {
@@ -549,22 +577,29 @@ private void handleCreateLinkResult(
549577
}
550578
}
551579

552-
private String fetchUPIDFromCQN(CqnSelect select) {
580+
private String fetchUPIDFromCQN(CqnSelect select, CdsEntity parentEntity) {
553581
try {
554582
String upID = null;
555583
ObjectMapper mapper = new ObjectMapper();
556584
JsonNode root = mapper.readTree(select.toString());
557585
JsonNode refArray = root.path("SELECT").path("from").path("ref");
558586
JsonNode secondLast = refArray.get(refArray.size() - 2);
559587
JsonNode whereArray = secondLast.path("where");
588+
589+
// Get the actual key field names from the parent entity
590+
List<String> keyElementNames = getKeyElementNames(parentEntity);
591+
560592
for (int i = 0; i < whereArray.size(); i++) {
561593
JsonNode node = whereArray.get(i);
562-
if (node.has("ref")
563-
&& node.get("ref").isArray()
564-
&& node.get("ref").get(0).asText().equals("ID")) {
565-
JsonNode valNode = whereArray.get(i + 2);
566-
upID = valNode.path("val").asText();
567-
break;
594+
595+
if (node.has("ref") && node.get("ref").isArray()) {
596+
String fieldName = node.get("ref").get(0).asText();
597+
598+
if (keyElementNames.contains(fieldName) && !fieldName.equals("IsActiveEntity")) {
599+
JsonNode valNode = whereArray.get(i + 2);
600+
upID = valNode.path("val").asText();
601+
break;
602+
}
568603
}
569604
}
570605
if (upID == null) {

sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java

Lines changed: 98 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,19 @@ void testCreate_shouldCreateLink() throws IOException {
163163
.thenReturn(Optional.of(draftEntity));
164164

165165
when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity));
166+
167+
// Mock parent entity for key extraction
168+
CdsEntity mockParentEntity = mock(CdsEntity.class);
169+
CdsElement mockKeyElement = mock(CdsElement.class);
170+
when(mockKeyElement.isKey()).thenReturn(true);
171+
when(mockKeyElement.getName()).thenReturn("ID");
172+
when(mockParentEntity.elements()).thenReturn(Stream.of(mockKeyElement));
173+
when(cdsModel.findEntity("MyService.MyEntity")).thenReturn(Optional.of(mockParentEntity));
166174
when(mockContext.getEvent()).thenReturn("createLink");
167175
CqnSelect cqnSelect = mock(CqnSelect.class);
168176
when(cqnSelect.toString())
169177
.thenReturn(
170-
"{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}");
178+
"{\"SELECT\":{\"from\":{\"ref\":[{\"id\":\"MyService.MyEntity\",\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}");
171179
when(mockContext.get("cqn")).thenReturn(cqnSelect);
172180
when(mockContext.get("name")).thenReturn("testURL");
173181
when(mockContext.get("url")).thenReturn("http://test-url");
@@ -262,11 +270,20 @@ void testCreate_ShouldThrowSpecifiedExceptionWhenMaxCountReached() throws IOExce
262270
.thenReturn(Optional.of(draftEntity));
263271

264272
when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity));
273+
274+
// Mock parent entity for key extraction
275+
CdsEntity mockParentEntity = mock(CdsEntity.class);
276+
CdsElement mockKeyElement = mock(CdsElement.class);
277+
when(mockKeyElement.isKey()).thenReturn(true);
278+
when(mockKeyElement.getName()).thenReturn("ID");
279+
when(mockParentEntity.elements()).thenReturn(Stream.of(mockKeyElement));
280+
when(cdsModel.findEntity("MyService.MyEntity")).thenReturn(Optional.of(mockParentEntity));
281+
265282
when(mockContext.getEvent()).thenReturn("createLink");
266283
CqnSelect cqnSelect = mock(CqnSelect.class);
267284
when(cqnSelect.toString())
268285
.thenReturn(
269-
"{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}");
286+
"{\"SELECT\":{\"from\":{\"ref\":[{\"id\":\"MyService.MyEntity\",\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}");
270287
when(mockContext.get("cqn")).thenReturn(cqnSelect);
271288
when(mockContext.get("name")).thenReturn("testURL");
272289
when(mockContext.get("url")).thenReturn("http://test-url");
@@ -327,11 +344,20 @@ void testCreate_ShouldThrowDefaultExceptionWhenMaxCountReached() throws IOExcept
327344
.thenReturn(Optional.of(draftEntity));
328345

329346
when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity));
347+
348+
// Mock parent entity for key extraction
349+
CdsEntity mockParentEntity = mock(CdsEntity.class);
350+
CdsElement mockKeyElement = mock(CdsElement.class);
351+
when(mockKeyElement.isKey()).thenReturn(true);
352+
when(mockKeyElement.getName()).thenReturn("ID");
353+
when(mockParentEntity.elements()).thenReturn(Stream.of(mockKeyElement));
354+
when(cdsModel.findEntity("MyService.MyEntity")).thenReturn(Optional.of(mockParentEntity));
355+
330356
when(mockContext.getEvent()).thenReturn("createLink");
331357
CqnSelect cqnSelect = mock(CqnSelect.class);
332358
when(cqnSelect.toString())
333359
.thenReturn(
334-
"{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}");
360+
"{\"SELECT\":{\"from\":{\"ref\":[{\"id\":\"MyService.MyEntity\",\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}");
335361
when(mockContext.get("cqn")).thenReturn(cqnSelect);
336362
when(mockContext.get("name")).thenReturn("testURL");
337363
when(mockContext.get("url")).thenReturn("http://test-url");
@@ -388,11 +414,20 @@ void testCreate_ShouldThrowExceptionWhenRestrictedCharacterInLinkName() throws I
388414
.thenReturn(Optional.of(draftEntity));
389415

390416
when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity));
417+
418+
// Mock parent entity for key extraction
419+
CdsEntity mockParentEntity = mock(CdsEntity.class);
420+
CdsElement mockKeyElement = mock(CdsElement.class);
421+
when(mockKeyElement.isKey()).thenReturn(true);
422+
when(mockKeyElement.getName()).thenReturn("ID");
423+
when(mockParentEntity.elements()).thenReturn(Stream.of(mockKeyElement));
424+
when(cdsModel.findEntity("MyService.MyEntity")).thenReturn(Optional.of(mockParentEntity));
425+
391426
when(mockContext.getEvent()).thenReturn("createLink");
392427
CqnSelect cqnSelect = mock(CqnSelect.class);
393428
when(cqnSelect.toString())
394429
.thenReturn(
395-
"{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}");
430+
"{\"SELECT\":{\"from\":{\"ref\":[{\"id\":\"MyService.MyEntity\",\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}");
396431
when(mockContext.get("cqn")).thenReturn(cqnSelect);
397432
when(mockContext.get("name")).thenReturn("test/URL");
398433
when(mockContext.get("url")).thenReturn("http://test-url");
@@ -450,11 +485,20 @@ void testCreate_ThrowsServiceExceptionOnDuplicateFile() throws IOException {
450485
.thenReturn(Optional.of(draftEntity));
451486

452487
when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity));
488+
489+
// Mock parent entity for key extraction
490+
CdsEntity mockParentEntity = mock(CdsEntity.class);
491+
CdsElement mockKeyElement = mock(CdsElement.class);
492+
when(mockKeyElement.isKey()).thenReturn(true);
493+
when(mockKeyElement.getName()).thenReturn("ID");
494+
when(mockParentEntity.elements()).thenReturn(Stream.of(mockKeyElement));
495+
when(cdsModel.findEntity("MyService.MyEntity")).thenReturn(Optional.of(mockParentEntity));
496+
453497
when(mockContext.getEvent()).thenReturn("createLink");
454498
CqnSelect cqnSelect = mock(CqnSelect.class);
455499
when(cqnSelect.toString())
456500
.thenReturn(
457-
"{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}");
501+
"{\"SELECT\":{\"from\":{\"ref\":[{\"id\":\"MyService.MyEntity\",\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}");
458502
when(mockContext.get("cqn")).thenReturn(cqnSelect);
459503
when(mockContext.get("name")).thenReturn("duplicateFile.txt");
460504
when(mockContext.get("url")).thenReturn("http://test-url");
@@ -514,11 +558,20 @@ void testCreate_ThrowsServiceException_WhenCreateDocumentThrowsException() throw
514558
.thenReturn(Optional.of(draftEntity));
515559

516560
when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity));
561+
562+
// Mock parent entity for key extraction
563+
CdsEntity mockParentEntity = mock(CdsEntity.class);
564+
CdsElement mockKeyElement = mock(CdsElement.class);
565+
when(mockKeyElement.isKey()).thenReturn(true);
566+
when(mockKeyElement.getName()).thenReturn("ID");
567+
when(mockParentEntity.elements()).thenReturn(Stream.of(mockKeyElement));
568+
when(cdsModel.findEntity("MyService.MyEntity")).thenReturn(Optional.of(mockParentEntity));
569+
517570
when(mockContext.getEvent()).thenReturn("createLink");
518571
CqnSelect cqnSelect = mock(CqnSelect.class);
519572
when(cqnSelect.toString())
520573
.thenReturn(
521-
"{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}");
574+
"{\"SELECT\":{\"from\":{\"ref\":[{\"id\":\"MyService.MyEntity\",\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}");
522575
when(mockContext.get("cqn")).thenReturn(cqnSelect);
523576
when(mockContext.get("name")).thenReturn("testURL");
524577
when(mockContext.get("url")).thenReturn("http://test-url");
@@ -585,11 +638,20 @@ void testCreate_ThrowsServiceExceptionOnDuplicateStatus() throws IOException {
585638
.thenReturn(Optional.of(draftEntity));
586639

587640
when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity));
641+
642+
// Mock parent entity for key extraction
643+
CdsEntity mockParentEntity = mock(CdsEntity.class);
644+
CdsElement mockKeyElement = mock(CdsElement.class);
645+
when(mockKeyElement.isKey()).thenReturn(true);
646+
when(mockKeyElement.getName()).thenReturn("ID");
647+
when(mockParentEntity.elements()).thenReturn(Stream.of(mockKeyElement));
648+
when(cdsModel.findEntity("MyService.MyEntity")).thenReturn(Optional.of(mockParentEntity));
649+
588650
when(mockContext.getEvent()).thenReturn("createLink");
589651
CqnSelect cqnSelect = mock(CqnSelect.class);
590652
when(cqnSelect.toString())
591653
.thenReturn(
592-
"{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}");
654+
"{\"SELECT\":{\"from\":{\"ref\":[{\"id\":\"MyService.MyEntity\",\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}");
593655
when(mockContext.get("cqn")).thenReturn(cqnSelect);
594656
when(mockContext.get("name")).thenReturn("duplicateFile.txt");
595657
when(mockContext.get("url")).thenReturn("http://test-url");
@@ -657,11 +719,20 @@ void testCreate_ThrowsServiceExceptionOnFailStatus() throws IOException {
657719
.thenReturn(Optional.of(draftEntity));
658720

659721
when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity));
722+
723+
// Mock parent entity for key extraction
724+
CdsEntity mockParentEntity = mock(CdsEntity.class);
725+
CdsElement mockKeyElement = mock(CdsElement.class);
726+
when(mockKeyElement.isKey()).thenReturn(true);
727+
when(mockKeyElement.getName()).thenReturn("ID");
728+
when(mockParentEntity.elements()).thenReturn(Stream.of(mockKeyElement));
729+
when(cdsModel.findEntity("MyService.MyEntity")).thenReturn(Optional.of(mockParentEntity));
730+
660731
when(mockContext.getEvent()).thenReturn("createLink");
661732
CqnSelect cqnSelect = mock(CqnSelect.class);
662733
when(cqnSelect.toString())
663734
.thenReturn(
664-
"{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}");
735+
"{\"SELECT\":{\"from\":{\"ref\":[{\"id\":\"MyService.MyEntity\",\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}");
665736
when(mockContext.get("cqn")).thenReturn(cqnSelect);
666737
when(mockContext.get("name")).thenReturn("duplicateFile.txt");
667738
when(mockContext.get("url")).thenReturn("http://test-url");
@@ -728,11 +799,20 @@ void testCreate_ThrowsServiceExceptionOnUnauthorizedStatus() throws IOException
728799
.thenReturn(Optional.of(draftEntity));
729800

730801
when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity));
802+
803+
// Mock parent entity for key extraction
804+
CdsEntity mockParentEntity = mock(CdsEntity.class);
805+
CdsElement mockKeyElement = mock(CdsElement.class);
806+
when(mockKeyElement.isKey()).thenReturn(true);
807+
when(mockKeyElement.getName()).thenReturn("ID");
808+
when(mockParentEntity.elements()).thenReturn(Stream.of(mockKeyElement));
809+
when(cdsModel.findEntity("MyService.MyEntity")).thenReturn(Optional.of(mockParentEntity));
810+
731811
when(mockContext.getEvent()).thenReturn("createLink");
732812
CqnSelect cqnSelect = mock(CqnSelect.class);
733813
when(cqnSelect.toString())
734814
.thenReturn(
735-
"{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}");
815+
"{\"SELECT\":{\"from\":{\"ref\":[{\"id\":\"MyService.MyEntity\",\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}");
736816
when(mockContext.get("cqn")).thenReturn(cqnSelect);
737817
when(mockContext.get("name")).thenReturn("duplicateFile.txt");
738818
when(mockContext.get("url")).thenReturn("http://test-url");
@@ -1148,6 +1228,9 @@ void testCreateLink_AttachmentCountConstraintExceeded() throws IOException {
11481228
when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity));
11491229
when(mockContext.getEvent()).thenReturn("createLink");
11501230
CqnSelect cqnSelect = mock(CqnSelect.class);
1231+
when(cqnSelect.toString())
1232+
.thenReturn(
1233+
"{\"SELECT\":{\"from\":{\"ref\":[{\"id\":\"MyService.MyEntity\",\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}");
11511234
when(mockContext.get("cqn")).thenReturn(cqnSelect);
11521235
when(mockContext.get("name")).thenReturn("testURL");
11531236
when(mockContext.get("url")).thenReturn("http://test-url");
@@ -1206,6 +1289,9 @@ void testCreateLink_RestrictedCharactersInName() throws IOException {
12061289
when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity));
12071290
when(mockContext.getEvent()).thenReturn("createLink");
12081291
CqnSelect cqnSelect = mock(CqnSelect.class);
1292+
when(cqnSelect.toString())
1293+
.thenReturn(
1294+
"{\"SELECT\":{\"from\":{\"ref\":[{\"id\":\"MyService.MyEntity\",\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}");
12091295
when(mockContext.get("cqn")).thenReturn(cqnSelect);
12101296
when(mockContext.get("name")).thenReturn("test/invalid\\name");
12111297
when(mockContext.get("url")).thenReturn("http://test-url");
@@ -1260,6 +1346,9 @@ void testCreateLink_UnauthorizedError() throws IOException {
12601346
when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity));
12611347
when(mockContext.getEvent()).thenReturn("createLink");
12621348
CqnSelect cqnSelect = mock(CqnSelect.class);
1349+
when(cqnSelect.toString())
1350+
.thenReturn(
1351+
"{\"SELECT\":{\"from\":{\"ref\":[{\"id\":\"MyService.MyEntity\",\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}");
12631352
when(mockContext.get("cqn")).thenReturn(cqnSelect);
12641353
when(mockContext.get("name")).thenReturn("testURL");
12651354
when(mockContext.get("url")).thenReturn("http://test-url");

0 commit comments

Comments
 (0)