From 7b98188d0c70150069b8171beaf51379884e1be6 Mon Sep 17 00:00:00 2001 From: PujaDeshmukh17 Date: Wed, 29 Jul 2026 16:27:09 +0530 Subject: [PATCH 01/16] initial fix for no cqn error --- .../SDMCreateAttachmentsHandler.java | 20 +++++++++++++++++++ .../SDMUpdateAttachmentsHandler.java | 20 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandler.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandler.java index 01021a263..ce87e9abb 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandler.java @@ -660,6 +660,26 @@ private void cleanupReadonlyContextsForAttachments( logger.debug("No attachments found for composition: {}", attachmentCompositionName); } } + // Fallback: recursively remove SDM_READONLY_CONTEXT from any nested structure + // that fetchAttachments failed to resolve (e.g. deeply nested compositions) + removeReadonlyContextRecursively(entityData); + } + + @SuppressWarnings("unchecked") + private void removeReadonlyContextRecursively(Map data) { + if (data == null) { + return; + } + data.remove(SDM_READONLY_CONTEXT); + for (Object value : data.values()) { + if (value instanceof List) { + for (Object item : (List) value) { + if (item instanceof Map) { + removeReadonlyContextRecursively((Map) item); + } + } + } + } } private static class SDMAttachmentData { diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandler.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandler.java index 2f07d0643..9b77398ac 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandler.java @@ -658,5 +658,25 @@ private void cleanupReadonlyContextsForAttachments( logger.debug("No attachments found for composition: {}", attachmentCompositionName); } } + // Fallback: recursively remove SDM_READONLY_CONTEXT from any nested structure + // that fetchAttachments failed to resolve (e.g. deeply nested compositions) + removeReadonlyContextRecursively(entityData); + } + + @SuppressWarnings("unchecked") + private void removeReadonlyContextRecursively(Map data) { + if (data == null) { + return; + } + data.remove(SDM_READONLY_CONTEXT); + for (Object value : data.values()) { + if (value instanceof List) { + for (Object item : (List) value) { + if (item instanceof Map) { + removeReadonlyContextRecursively((Map) item); + } + } + } + } } } From 3726e23ffbe28eeb634231d5ce0f853e0900c050 Mon Sep 17 00:00:00 2001 From: PujaDeshmukh17 Date: Wed, 29 Jul 2026 16:33:13 +0530 Subject: [PATCH 02/16] Ut --- .../SDMCreateAttachmentsHandlerTest.java | 114 +++++++++++++++++ .../SDMUpdateAttachmentsHandlerTest.java | 121 ++++++++++++++++++ 2 files changed, 235 insertions(+) diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandlerTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandlerTest.java index 719a22edc..33cc1d179 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandlerTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandlerTest.java @@ -2,6 +2,7 @@ import static org.junit.Assert.assertNull; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyList; @@ -11,6 +12,7 @@ import com.sap.cds.CdsData; import com.sap.cds.reflect.*; import com.sap.cds.sdm.caching.CacheConfig; +import com.sap.cds.sdm.constants.SDMConstants; import com.sap.cds.sdm.handler.TokenHandler; import com.sap.cds.sdm.handler.applicationservice.SDMCreateAttachmentsHandler; import com.sap.cds.sdm.handler.applicationservice.helper.AttachmentsHandlerUtils; @@ -1024,4 +1026,116 @@ public void testUpdateActiveEntitySdmMetadata_CorrectFieldMapping() { return true; })); } + + // --- Tests for removeReadonlyContextRecursively fallback --- + + @Test + public void testCleanupReadonlyContexts_DirectAttachment_RemovesSDMReadonlyContext() + throws Exception { + CdsCreateEventContext ctx = mock(CdsCreateEventContext.class); + CdsEntity entity = mock(CdsEntity.class); + when(ctx.getTarget()).thenReturn(entity); + when(entity.getQualifiedName()).thenReturn("AdminService.Books"); + + Map attachment = new HashMap<>(); + attachment.put("ID", "att1"); + attachment.put(SDMConstants.SDM_READONLY_CONTEXT, Map.of("uploadStatus", "Success")); + + Map entityData = new HashMap<>(); + entityData.put("attachments", List.of(attachment)); + + Map> compositionDetails = new HashMap<>(); + Map info = new HashMap<>(); + info.put("name", "AdminService.Books.attachments"); + compositionDetails.put("AdminService.Books.attachments", info); + + java.lang.reflect.Method method = + SDMCreateAttachmentsHandler.class.getDeclaredMethod( + "cleanupReadonlyContextsForAttachments", + CdsCreateEventContext.class, + Map.class, + Map.class); + method.setAccessible(true); + method.invoke(handler, ctx, entityData, compositionDetails); + + assertFalse( + attachment.containsKey(SDMConstants.SDM_READONLY_CONTEXT), + "SDM_READONLY_CONTEXT should be removed from flat attachment"); + } + + @Test + public void testCleanupReadonlyContexts_DeeplyNestedAttachment_FallbackRemovesSDMReadonlyContext() + throws Exception { + // Simulates customer scenario: Books → chapters → sections → attachments + // fetchAttachments fails because parentKey 'Sections' (entity name) != 'sections' (property + // name) + CdsCreateEventContext ctx = mock(CdsCreateEventContext.class); + CdsEntity entity = mock(CdsEntity.class); + when(ctx.getTarget()).thenReturn(entity); + when(entity.getQualifiedName()).thenReturn("AdminService.Books"); + + Map attachment = new HashMap<>(); + attachment.put("ID", "att-nested"); + attachment.put(SDMConstants.SDM_READONLY_CONTEXT, Map.of("uploadStatus", "Success")); + + Map section = new HashMap<>(); + section.put("ID", "sec1"); + section.put("attachments", List.of(attachment)); + + Map chapter = new HashMap<>(); + chapter.put("ID", "chap1"); + chapter.put("sections", List.of(section)); // property name 'sections' != entity name 'Sections' + + Map entityData = new HashMap<>(); + entityData.put("cHapters", List.of(chapter)); + + Map> compositionDetails = new HashMap<>(); + Map info = new HashMap<>(); + info.put( + "name", + "AdminService.Sections.attachments"); // parentKeyFromComposition = 'Sections' — mismatch + compositionDetails.put("AdminService.Sections.attachments", info); + + java.lang.reflect.Method method = + SDMCreateAttachmentsHandler.class.getDeclaredMethod( + "cleanupReadonlyContextsForAttachments", + CdsCreateEventContext.class, + Map.class, + Map.class); + method.setAccessible(true); + method.invoke(handler, ctx, entityData, compositionDetails); + + assertFalse( + attachment.containsKey(SDMConstants.SDM_READONLY_CONTEXT), + "SDM_READONLY_CONTEXT should be removed from deeply nested attachment by fallback"); + } + + @Test + public void testCleanupReadonlyContexts_EmptyCompositionDetails_FallbackStillCleansUp() + throws Exception { + CdsCreateEventContext ctx = mock(CdsCreateEventContext.class); + CdsEntity entity = mock(CdsEntity.class); + when(ctx.getTarget()).thenReturn(entity); + when(entity.getQualifiedName()).thenReturn("AdminService.Books"); + + Map attachment = new HashMap<>(); + attachment.put("ID", "att1"); + attachment.put(SDMConstants.SDM_READONLY_CONTEXT, Map.of("uploadStatus", "InProgress")); + + Map entityData = new HashMap<>(); + entityData.put("attachments", List.of(attachment)); + + java.lang.reflect.Method method = + SDMCreateAttachmentsHandler.class.getDeclaredMethod( + "cleanupReadonlyContextsForAttachments", + CdsCreateEventContext.class, + Map.class, + Map.class); + method.setAccessible(true); + method.invoke(handler, ctx, entityData, new HashMap<>()); + + assertFalse( + attachment.containsKey(SDMConstants.SDM_READONLY_CONTEXT), + "SDM_READONLY_CONTEXT should be removed even when compositionDetails is empty"); + } } diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandlerTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandlerTest.java index 745887f8d..c3f53384e 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandlerTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandlerTest.java @@ -1,5 +1,6 @@ package unit.com.sap.cds.sdm.handler.applicationservice; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -945,6 +946,126 @@ public void testRenameWithNoAttachments() throws IOException { // } // } + // --- Tests for removeReadonlyContextRecursively fallback --- + + @Test + public void testCleanupReadonlyContexts_DirectAttachment_RemovesSDMReadonlyContext() + throws Exception { + // SDM_READONLY_CONTEXT on a direct (flat) attachment is removed + CdsUpdateEventContext ctx = mock(CdsUpdateEventContext.class); + CdsEntity entity = mock(CdsEntity.class); + when(ctx.getTarget()).thenReturn(entity); + when(entity.getQualifiedName()).thenReturn("AdminService.Books"); + + Map attachment = new HashMap<>(); + attachment.put("ID", "att1"); + attachment.put(SDMConstants.SDM_READONLY_CONTEXT, Map.of("uploadStatus", "Success")); + + List> attachments = new ArrayList<>(); + attachments.add(attachment); + + Map entityData = new HashMap<>(); + entityData.put("attachments", attachments); + + Map> compositionDetails = new HashMap<>(); + Map info = new HashMap<>(); + info.put("name", "AdminService.Books.attachments"); + compositionDetails.put("AdminService.Books.attachments", info); + + java.lang.reflect.Method method = + SDMUpdateAttachmentsHandler.class.getDeclaredMethod( + "cleanupReadonlyContextsForAttachments", + CdsUpdateEventContext.class, + Map.class, + Map.class); + method.setAccessible(true); + method.invoke(handler, ctx, entityData, compositionDetails); + + assertFalse( + attachment.containsKey(SDMConstants.SDM_READONLY_CONTEXT), + "SDM_READONLY_CONTEXT should be removed from flat attachment"); + } + + @Test + public void testCleanupReadonlyContexts_DeeplyNestedAttachment_FallbackRemovesSDMReadonlyContext() + throws Exception { + // Simulates customer scenario: Books → chapters → sections → attachments + // fetchAttachments fails to find the attachment because parentKey 'Sections' (entity name) + // does not match 'sections' (property name in payload) — fallback must clean it up + CdsUpdateEventContext ctx = mock(CdsUpdateEventContext.class); + CdsEntity entity = mock(CdsEntity.class); + when(ctx.getTarget()).thenReturn(entity); + when(entity.getQualifiedName()).thenReturn("AdminService.Books"); + + Map attachment = new HashMap<>(); + attachment.put("ID", "att-nested"); + attachment.put(SDMConstants.SDM_READONLY_CONTEXT, Map.of("uploadStatus", "Success")); + + List> attachments = new ArrayList<>(); + attachments.add(attachment); + + Map section = new HashMap<>(); + section.put("ID", "sec1"); + section.put("attachments", attachments); + + Map chapter = new HashMap<>(); + chapter.put("ID", "chap1"); + chapter.put("sections", List.of(section)); // property name 'sections' != entity name 'Sections' + + Map entityData = new HashMap<>(); + entityData.put("cHapters", List.of(chapter)); + + // Composition name uses entity name 'Sections' — parentKeyFromComposition = 'Sections' + // but entityData key is 'sections' → fetchAttachments returns empty → fallback must handle it + Map> compositionDetails = new HashMap<>(); + Map info = new HashMap<>(); + info.put("name", "AdminService.Sections.attachments"); + compositionDetails.put("AdminService.Sections.attachments", info); + + java.lang.reflect.Method method = + SDMUpdateAttachmentsHandler.class.getDeclaredMethod( + "cleanupReadonlyContextsForAttachments", + CdsUpdateEventContext.class, + Map.class, + Map.class); + method.setAccessible(true); + method.invoke(handler, ctx, entityData, compositionDetails); + + assertFalse( + attachment.containsKey(SDMConstants.SDM_READONLY_CONTEXT), + "SDM_READONLY_CONTEXT should be removed from deeply nested attachment by fallback"); + } + + @Test + public void testCleanupReadonlyContexts_EmptyCompositionDetails_FallbackStillCleansUp() + throws Exception { + // When getAttachmentCompositionDetails returns empty (e.g. on error), fallback still cleans up + CdsUpdateEventContext ctx = mock(CdsUpdateEventContext.class); + CdsEntity entity = mock(CdsEntity.class); + when(ctx.getTarget()).thenReturn(entity); + when(entity.getQualifiedName()).thenReturn("AdminService.Books"); + + Map attachment = new HashMap<>(); + attachment.put("ID", "att1"); + attachment.put(SDMConstants.SDM_READONLY_CONTEXT, Map.of("uploadStatus", "InProgress")); + + Map entityData = new HashMap<>(); + entityData.put("attachments", List.of(attachment)); + + java.lang.reflect.Method method = + SDMUpdateAttachmentsHandler.class.getDeclaredMethod( + "cleanupReadonlyContextsForAttachments", + CdsUpdateEventContext.class, + Map.class, + Map.class); + method.setAccessible(true); + method.invoke(handler, ctx, entityData, new HashMap<>()); + + assertFalse( + attachment.containsKey(SDMConstants.SDM_READONLY_CONTEXT), + "SDM_READONLY_CONTEXT should be removed even when compositionDetails is empty"); + } + private List prepareMockAttachmentData(String... fileNames) { List data = new ArrayList<>(); for (String fileName : fileNames) { From c34468eabdbf90f37254e15b6cf30b3b37eea2bc Mon Sep 17 00:00:00 2001 From: PujaDeshmukh17 Date: Thu, 30 Jul 2026 16:35:39 +0530 Subject: [PATCH 03/16] Added logs --- .../SDMCreateAttachmentsHandler.java | 83 ++++++++++------ .../SDMUpdateAttachmentsHandler.java | 94 +++++++++++++------ 2 files changed, 121 insertions(+), 56 deletions(-) diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandler.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandler.java index ce87e9abb..ca12ecdbf 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandler.java @@ -109,7 +109,7 @@ public void processBefore(CdsCreateEventContext context, List data) thr logger.info( "START: Process attachments before persistence for entity: {}", context.getTarget().getQualifiedName()); - logger.debug("Number of entities to process: {}", data.size()); + logger.info("Number of entities to process: {}", data.size()); for (CdsData entityData : data) { Map> attachmentCompositionDetails = @@ -119,7 +119,7 @@ public void processBefore(CdsCreateEventContext context, List data) thr persistenceService, context.getTarget().getQualifiedName(), entityData); - logger.debug("Attachment compositions present: {}", attachmentCompositionDetails.keySet()); + logger.info("Attachment compositions found: {}", attachmentCompositionDetails.keySet()); updateName(context, data, attachmentCompositionDetails); // Remove uploadStatus from attachment data to prevent validation errors cleanupReadonlyContextsForAttachments(context, entityData, attachmentCompositionDetails); @@ -151,32 +151,45 @@ public void processAfter(CdsCreateEventContext context, List data) { Optional attachmentEntity = context.getModel().findEntity(attachmentCompositionDefinition); - if (attachmentEntity.isPresent()) { - String targetEntity = context.getTarget().getQualifiedName(); - List> attachments = - AttachmentsHandlerUtils.fetchAttachments( - targetEntity, entityData, attachmentCompositionName); + if (!attachmentEntity.isPresent()) { + logger.warn( + "[SDM] CREATE: Attachment entity '{}' not found in CDS model — skipping uploadStatus persistence for composition '{}'", + attachmentCompositionDefinition, + attachmentCompositionName); + continue; + } - if (attachments != null) { - logger.debug( - "Processing {} attachments for composition: {}", - attachments.size(), - attachmentCompositionName); - for (Map attachment : attachments) { - String id = (String) attachment.get("ID"); - String uploadStatus = (String) attachment.get("uploadStatus"); - if (id != null) { - CmisDocument cmisDocument = new CmisDocument(); - cmisDocument.setAttachmentId(id); - cmisDocument.setUploadStatus(uploadStatus); - logger.debug("Saving uploadStatus: {} for attachment ID: {}", uploadStatus, id); - // Update uploadStatus to Success in database if it was InProgress - dbQuery.saveUploadStatusToAttachment( - attachmentEntity.get(), persistenceService, cmisDocument); - totalProcessed++; - } + String targetEntity = context.getTarget().getQualifiedName(); + List> attachments = + AttachmentsHandlerUtils.fetchAttachments( + targetEntity, entityData, attachmentCompositionName); + + if (attachments != null && !attachments.isEmpty()) { + logger.info( + "[SDM] CREATE: Persisting uploadStatus for {} attachment(s) in composition '{}'", + attachments.size(), + attachmentCompositionName); + for (Map attachment : attachments) { + String id = (String) attachment.get("ID"); + String uploadStatus = (String) attachment.get("uploadStatus"); + if (id != null) { + logger.debug("Saving uploadStatus '{}' for attachment ID: {}", uploadStatus, id); + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setAttachmentId(id); + cmisDocument.setUploadStatus(uploadStatus); + dbQuery.saveUploadStatusToAttachment( + attachmentEntity.get(), persistenceService, cmisDocument); + totalProcessed++; + } else { + logger.warn( + "[SDM] CREATE: Attachment in composition '{}' has no ID — skipping uploadStatus persistence", + attachmentCompositionName); } } + } else { + logger.debug( + "No attachments in payload for composition '{}' during post-processing", + attachmentCompositionName); } } } @@ -657,11 +670,20 @@ private void cleanupReadonlyContextsForAttachments( } } } else { - logger.debug("No attachments found for composition: {}", attachmentCompositionName); + logger.warn( + "[SDM] CREATE: fetchAttachments returned no results for composition '{}' on entity '{}'. " + + "This may indicate a deeply nested composition whose property name does not match the entity name. " + + "Fallback recursive cleanup will handle SDM_READONLY_CONTEXT removal.", + attachmentCompositionName, + targetEntity); } } // Fallback: recursively remove SDM_READONLY_CONTEXT from any nested structure // that fetchAttachments failed to resolve (e.g. deeply nested compositions) + logger.info( + "[SDM] CREATE: Running recursive fallback to remove SDM_READONLY_CONTEXT from entity '{}'. " + + "Any WARN entries above indicate compositions where fetchAttachments could not resolve attachments.", + targetEntity); removeReadonlyContextRecursively(entityData); } @@ -670,7 +692,14 @@ private void removeReadonlyContextRecursively(Map data) { if (data == null) { return; } - data.remove(SDM_READONLY_CONTEXT); + if (data.containsKey(SDM_READONLY_CONTEXT)) { + logger.warn( + "[SDM] CREATE: Fallback removed SDM_READONLY_CONTEXT from map with keys: {}. " + + "This entry was not cleaned up by the composition-based path — " + + "likely a deeply nested or mismatched composition name.", + data.keySet()); + data.remove(SDM_READONLY_CONTEXT); + } for (Object value : data.values()) { if (value instanceof List) { for (Object item : (List) value) { diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandler.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandler.java index 9b77398ac..65126256d 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandler.java @@ -85,32 +85,45 @@ public void processAfter(CdsUpdateEventContext context, List data) { Optional attachmentEntity = context.getModel().findEntity(attachmentCompositionDefinition); - if (attachmentEntity.isPresent()) { - String targetEntity = context.getTarget().getQualifiedName(); - List> attachments = - AttachmentsHandlerUtils.fetchAttachments( - targetEntity, entityData, attachmentCompositionName); + if (!attachmentEntity.isPresent()) { + logger.warn( + "[SDM] UPDATE: Attachment entity '{}' not found in CDS model — skipping uploadStatus persistence for composition '{}'", + attachmentCompositionDefinition, + attachmentCompositionName); + continue; + } - if (attachments != null) { - logger.debug( - "Processing {} attachments for composition: {}", - attachments.size(), - attachmentCompositionName); - for (Map attachment : attachments) { - String id = (String) attachment.get("ID"); - String uploadStatus = (String) attachment.get("uploadStatus"); - if (id != null) { - CmisDocument cmisDocument = new CmisDocument(); - cmisDocument.setAttachmentId(id); - cmisDocument.setUploadStatus(uploadStatus); - // Update uploadStatus to Success in database if it was InProgress - logger.debug("Saving uploadStatus: {} for attachment ID: {}", uploadStatus, id); - dbQuery.saveUploadStatusToAttachment( - attachmentEntity.get(), persistenceService, cmisDocument); - totalProcessed++; - } + String targetEntity = context.getTarget().getQualifiedName(); + List> attachments = + AttachmentsHandlerUtils.fetchAttachments( + targetEntity, entityData, attachmentCompositionName); + + if (attachments != null && !attachments.isEmpty()) { + logger.info( + "[SDM] UPDATE: Persisting uploadStatus for {} attachment(s) in composition '{}'", + attachments.size(), + attachmentCompositionName); + for (Map attachment : attachments) { + String id = (String) attachment.get("ID"); + String uploadStatus = (String) attachment.get("uploadStatus"); + if (id != null) { + logger.debug("Saving uploadStatus '{}' for attachment ID: {}", uploadStatus, id); + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setAttachmentId(id); + cmisDocument.setUploadStatus(uploadStatus); + dbQuery.saveUploadStatusToAttachment( + attachmentEntity.get(), persistenceService, cmisDocument); + totalProcessed++; + } else { + logger.warn( + "[SDM] UPDATE: Attachment in composition '{}' has no ID — skipping uploadStatus persistence", + attachmentCompositionName); } } + } else { + logger.debug( + "No attachments in payload for composition '{}' during post-processing", + attachmentCompositionName); } } } @@ -123,7 +136,7 @@ public void processBefore(CdsUpdateEventContext context, List data) thr logger.info( "START: Process attachments before persistence for entity: {}", context.getTarget().getQualifiedName()); - logger.debug("Number of entities to update: {}", data.size()); + logger.info("Number of entities to update: {}", data.size()); // Get comprehensive attachment composition details for each entity for (CdsData entityData : data) { @@ -134,7 +147,7 @@ public void processBefore(CdsUpdateEventContext context, List data) thr persistenceService, context.getTarget().getQualifiedName(), entityData); - logger.debug("Attachment compositions present: {}", attachmentCompositionDetails.keySet()); + logger.info("Attachment compositions found: {}", attachmentCompositionDetails.keySet()); updateName(context, data, attachmentCompositionDetails); @@ -208,6 +221,10 @@ private void renameDocument( if (attachments != null && !attachments.isEmpty()) { propertyTitles = SDMUtils.getPropertyTitles(attachmentEntity, attachments.get(0)); } else { + logger.info( + "[SDM] UPDATE: No attachments in payload for composition '{}' on entity '{}' — skipping rename", + attachmentCompositionName, + targetEntity); propertyTitles = null; } if (attachments != null && !attachments.isEmpty()) { @@ -366,7 +383,10 @@ public void processAttachment( propertiesInDB); if (updatedSecondaryProperties.isEmpty()) { - logger.debug("No changes detected for attachment ID: {}, skipping SDM update", id); + logger.info( + "[SDM] UPDATE: No property changes detected for attachment ID: {} (fileName: '{}') — skipping SDM call", + id, + filenameInRequest); return; } @@ -513,7 +533,7 @@ private void updateAttachmentInSDM( secondaryPropertiesWithInvalidDefinitions, context.getUserInfo().isSystemUser()); - logger.debug("SDM update response code: {} for attachment ID: {}", responseCode, id); + logger.info("SDM update response code: {} for attachment ID: {}", responseCode, id); AttachmentsHandlerUtils.handleSDMUpdateResponse( responseCode, @@ -655,11 +675,20 @@ private void cleanupReadonlyContextsForAttachments( } } } else { - logger.debug("No attachments found for composition: {}", attachmentCompositionName); + logger.warn( + "[SDM] UPDATE: fetchAttachments returned no results for composition '{}' on entity '{}'. " + + "This may indicate a deeply nested composition whose property name does not match the entity name. " + + "Fallback recursive cleanup will handle SDM_READONLY_CONTEXT removal.", + attachmentCompositionName, + targetEntity); } } // Fallback: recursively remove SDM_READONLY_CONTEXT from any nested structure // that fetchAttachments failed to resolve (e.g. deeply nested compositions) + logger.info( + "[SDM] UPDATE: Running recursive fallback to remove SDM_READONLY_CONTEXT from entity '{}'. " + + "Any WARN entries above indicate compositions where fetchAttachments could not resolve attachments.", + targetEntity); removeReadonlyContextRecursively(entityData); } @@ -668,7 +697,14 @@ private void removeReadonlyContextRecursively(Map data) { if (data == null) { return; } - data.remove(SDM_READONLY_CONTEXT); + if (data.containsKey(SDM_READONLY_CONTEXT)) { + logger.warn( + "[SDM] UPDATE: Fallback removed SDM_READONLY_CONTEXT from map with keys: {}. " + + "This entry was not cleaned up by the composition-based path — " + + "likely a deeply nested or mismatched composition name.", + data.keySet()); + data.remove(SDM_READONLY_CONTEXT); + } for (Object value : data.values()) { if (value instanceof List) { for (Object item : (List) value) { From 167fdf5e5306ecad17642427032ec794e6a42a2f Mon Sep 17 00:00:00 2001 From: vibhutikumar <160819926+vibhutikumar07@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:29:01 +0530 Subject: [PATCH 04/16] Create action.yml --- .../deploy-central-snapshot/action.yml | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 .github/actions/deploy-central-snapshot/action.yml diff --git a/.github/actions/deploy-central-snapshot/action.yml b/.github/actions/deploy-central-snapshot/action.yml new file mode 100644 index 000000000..f4c25f5b9 --- /dev/null +++ b/.github/actions/deploy-central-snapshot/action.yml @@ -0,0 +1,77 @@ +name: Deploy Snapshot to Central Portal +description: "Deploys a Maven SNAPSHOT package to Sonatype Central Portal Snapshots repository." + +inputs: + user: + description: "Sonatype Central Portal username (same as Maven Central)" + required: true + password: + description: "Sonatype Central Portal password (same as Maven Central)" + required: true + pgp-pub-key: + description: "The public pgp key ID (optional for snapshots but recommended)" + required: false + pgp-private-key: + description: "The private pgp key (optional for snapshots but recommended)" + required: false + pgp-passphrase: + description: "The passphrase for pgp (optional for snapshots but recommended)" + required: false + +runs: + using: composite + steps: + - name: "Setup Java" + uses: actions/setup-java@v4 + with: + distribution: 'sapmachine' + java-version: '21' + cache: maven + server-id: central + server-username: CENTRAL_USER + server-password: CENTRAL_PASSWORD + + - name: "Import GPG Key (if provided)" + if: inputs.pgp-private-key != '' + run: | + echo "${{ inputs.pgp-private-key }}" | gpg --batch --passphrase "$PASSPHRASE" --import + shell: bash + env: + PASSPHRASE: ${{ inputs.pgp-passphrase }} + + - name: "Verify SNAPSHOT version" + run: | + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + echo "Current version: $VERSION" + if [[ ! "$VERSION" == *-SNAPSHOT ]]; then + echo "Error: Version $VERSION is not a SNAPSHOT version!" + echo "Central Portal Snapshots repository only accepts SNAPSHOT versions." + exit 1 + fi + echo "✅ Version $VERSION is a valid SNAPSHOT" + shell: bash + + - name: "Deploy Snapshot to Central Portal" + run: | + echo "🚀 Deploying SNAPSHOT to Sonatype Central Portal..." + if [ -n "$GPG_PASSPHRASE" ] && [ -n "$GPG_PUB_KEY" ]; then + mvn -B -ntp --show-version \ + -Dmaven.install.skip=true \ + -Dmaven.test.skip=true \ + -Dgpg.passphrase="$GPG_PASSPHRASE" \ + -Dgpg.keyname="$GPG_PUB_KEY" \ + clean deploy -P deploy-central-snapshot + else + mvn -B -ntp --show-version \ + -Dmaven.install.skip=true \ + -Dmaven.test.skip=true \ + -Dgpg.skip=true \ + clean deploy -P deploy-central-snapshot + fi + echo "✅ Snapshot deployed successfully!" + shell: bash + env: + CENTRAL_USER: ${{ inputs.user }} + CENTRAL_PASSWORD: ${{ inputs.password }} + GPG_PASSPHRASE: ${{ inputs.pgp-passphrase }} + GPG_PUB_KEY: ${{ inputs.pgp-pub-key }} From b9ec0cfb4ec6849d0926a53c24480c34847f1845 Mon Sep 17 00:00:00 2001 From: vibhutikumar <160819926+vibhutikumar07@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:30:21 +0530 Subject: [PATCH 05/16] Create deploy-central-snapshot.yml --- .github/workflows/deploy-central-snapshot.yml | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 .github/workflows/deploy-central-snapshot.yml diff --git a/.github/workflows/deploy-central-snapshot.yml b/.github/workflows/deploy-central-snapshot.yml new file mode 100644 index 000000000..dbf88f79b --- /dev/null +++ b/.github/workflows/deploy-central-snapshot.yml @@ -0,0 +1,136 @@ +name: Deploy Snapshot to Central Portal + +env: + JAVA_VERSION: '21' + +on: + # Manual trigger - select any branch from GitHub UI + workflow_dispatch: + inputs: + sign_artifacts: + description: 'Sign artifacts with GPG' + required: false + default: 'true' + type: choice + options: + - 'true' + - 'false' + + # Auto-trigger on push to this feature branch + push: + branches: + - snapshot_maven + +permissions: + contents: read + packages: read + +jobs: + verify-snapshot: + runs-on: ubuntu-latest + outputs: + is_snapshot: ${{ steps.check.outputs.is_snapshot }} + version: ${{ steps.check.outputs.version }} + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + cache: maven + + - name: Check version is SNAPSHOT + id: check + run: | + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + echo "version=$VERSION" >> $GITHUB_OUTPUT + if [[ "$VERSION" == *-SNAPSHOT ]]; then + echo "is_snapshot=true" >> $GITHUB_OUTPUT + echo "✅ Version $VERSION is a SNAPSHOT" + else + echo "is_snapshot=false" >> $GITHUB_OUTPUT + echo "❌ Version $VERSION is NOT a SNAPSHOT - deployment will be skipped" + fi + + build: + runs-on: ubuntu-latest + needs: verify-snapshot + if: needs.verify-snapshot.outputs.is_snapshot == 'true' + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + cache: maven + + - name: Build + run: | + echo "🔨 Building SNAPSHOT version: ${{ needs.verify-snapshot.outputs.version }}" + mvn clean install -P unit-tests -DskipIntegrationTests + echo "✅ Build completed successfully!" + + - name: Upload build artifacts + uses: actions/upload-artifact@v6 + with: + name: snapshot-build + path: . + include-hidden-files: true + retention-days: 1 + + deploy: + name: Deploy Snapshot to Central Portal + runs-on: ubuntu-latest + needs: [verify-snapshot, build] + if: needs.verify-snapshot.outputs.is_snapshot == 'true' + environment: maven-central + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Download artifact + uses: actions/download-artifact@v7 + with: + name: snapshot-build + + - name: Deploy Snapshot (with GPG signing) + if: github.event.inputs.sign_artifacts != 'false' + uses: ./.github/actions/deploy-central-snapshot + with: + user: ${{ secrets.CENTRAL_REPOSITORY_USER }} + password: ${{ secrets.CENTRAL_REPOSITORY_PASS }} + pgp-pub-key: ${{ secrets.PGP_PUB_KEY }} + pgp-private-key: ${{ secrets.PGP_PRIVATE_KEY }} + pgp-passphrase: ${{ secrets.PGP_PASSPHRASE }} + + - name: Deploy Snapshot (without GPG signing) + if: github.event.inputs.sign_artifacts == 'false' + uses: ./.github/actions/deploy-central-snapshot + with: + user: ${{ secrets.CENTRAL_REPOSITORY_USER }} + password: ${{ secrets.CENTRAL_REPOSITORY_PASS }} + + - name: Summary + run: | + echo "## 🚀 Snapshot Deployed to Central Portal" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Version:** ${{ needs.verify-snapshot.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Repository:** https://central.sonatype.com/repository/maven-snapshots/" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Usage" >> $GITHUB_STEP_SUMMARY + echo '```xml' >> $GITHUB_STEP_SUMMARY + echo '' >> $GITHUB_STEP_SUMMARY + echo ' ' >> $GITHUB_STEP_SUMMARY + echo ' central-snapshots' >> $GITHUB_STEP_SUMMARY + echo ' https://central.sonatype.com/repository/maven-snapshots/' >> $GITHUB_STEP_SUMMARY + echo ' true' >> $GITHUB_STEP_SUMMARY + echo ' ' >> $GITHUB_STEP_SUMMARY + echo '' >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY From c0730ff444d67f508ebd3c818ae1dcbfaa0614d8 Mon Sep 17 00:00:00 2001 From: vibhutikumar <160819926+vibhutikumar07@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:52:32 +0530 Subject: [PATCH 06/16] Remove artifact upload/download from snapshot workflow The composite action runs clean deploy internally so the artifact shuttle between build and deploy jobs was redundant and causing download failures due to workspace conflicts after checkout. --- .github/workflows/deploy-central-snapshot.yml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/.github/workflows/deploy-central-snapshot.yml b/.github/workflows/deploy-central-snapshot.yml index dbf88f79b..087083f14 100644 --- a/.github/workflows/deploy-central-snapshot.yml +++ b/.github/workflows/deploy-central-snapshot.yml @@ -76,14 +76,6 @@ jobs: mvn clean install -P unit-tests -DskipIntegrationTests echo "✅ Build completed successfully!" - - name: Upload build artifacts - uses: actions/upload-artifact@v6 - with: - name: snapshot-build - path: . - include-hidden-files: true - retention-days: 1 - deploy: name: Deploy Snapshot to Central Portal runs-on: ubuntu-latest @@ -94,11 +86,6 @@ jobs: - name: Checkout uses: actions/checkout@v6 - - name: Download artifact - uses: actions/download-artifact@v7 - with: - name: snapshot-build - - name: Deploy Snapshot (with GPG signing) if: github.event.inputs.sign_artifacts != 'false' uses: ./.github/actions/deploy-central-snapshot From d329a04f302849004995002e1434fb46fea5a2f6 Mon Sep 17 00:00:00 2001 From: vibhutikumar <160819926+vibhutikumar07@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:38:52 +0530 Subject: [PATCH 07/16] Update pom.xml Updated pom to use Central Portal snapshots --- pom.xml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pom.xml b/pom.xml index 250ac7fe1..4db0675e2 100644 --- a/pom.xml +++ b/pom.xml @@ -346,6 +346,35 @@ + + deploy-central-snapshot + + + + central + Sonatype Central Portal Snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + + + disabled-release + file:///dev/null + + + + + + org.sonatype.central + central-publishing-maven-plugin + + central + USER_MANAGED + true + + + + + From 9a55a38cb4eec1f577d4bfaa12f08355a6b5756c Mon Sep 17 00:00:00 2001 From: vibhutikumar <160819926+vibhutikumar07@users.noreply.github.com> Date: Tue, 21 Jul 2026 12:25:31 +0530 Subject: [PATCH 08/16] Add deploy-central-snapshot profile to sdm/pom.xml child module Child module had its own distributionManagement pointing to Artifactory. Without overriding it in the profile, Maven hits Artifactory for metadata during deploy and gets a 401. This profile redirects to Maven Central snapshots when -P deploy-central-snapshot is active. --- sdm/pom.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/sdm/pom.xml b/sdm/pom.xml index 8210bbc7d..58ceff0fd 100644 --- a/sdm/pom.xml +++ b/sdm/pom.xml @@ -97,6 +97,20 @@ + + deploy-central-snapshot + + + central + Sonatype Central Portal Snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + + disabled-release + file:///dev/null + + + From 137f0ea7359207164a10336440c0743832eff144 Mon Sep 17 00:00:00 2001 From: vibhutikumar <160819926+vibhutikumar07@users.noreply.github.com> Date: Wed, 29 Jul 2026 18:26:33 +0530 Subject: [PATCH 09/16] Updated the workflow --- .../actions/deploy-central-snapshot/action.yml | 15 ++++++++++++--- .github/workflows/deploy-central-snapshot.yml | 3 ++- pom.xml | 4 +--- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/actions/deploy-central-snapshot/action.yml b/.github/actions/deploy-central-snapshot/action.yml index f4c25f5b9..0d9bbd755 100644 --- a/.github/actions/deploy-central-snapshot/action.yml +++ b/.github/actions/deploy-central-snapshot/action.yml @@ -32,11 +32,15 @@ runs: server-password: CENTRAL_PASSWORD - name: "Import GPG Key (if provided)" - if: inputs.pgp-private-key != '' + if: ${{ inputs.pgp-private-key != '' }} run: | - echo "${{ inputs.pgp-private-key }}" | gpg --batch --passphrase "$PASSPHRASE" --import - shell: bash + set +x + echo "::add-mask::$PGP_PRIVATE_KEY" + echo "::add-mask::$PASSPHRASE" + echo "$PGP_PRIVATE_KEY" | gpg --batch --passphrase "$PASSPHRASE" --import + shell: bash env: + PGP_PRIVATE_KEY: ${{ inputs.pgp-private-key }} PASSPHRASE: ${{ inputs.pgp-passphrase }} - name: "Verify SNAPSHOT version" @@ -53,6 +57,11 @@ runs: - name: "Deploy Snapshot to Central Portal" run: | + set +x + echo "::add-mask::$CENTRAL_USER" + echo "::add-mask::$CENTRAL_PASSWORD" + [ -n "$GPG_PASSPHRASE" ] && echo "::add-mask::$GPG_PASSPHRASE" + [ -n "$GPG_PUB_KEY" ] && echo "::add-mask::$GPG_PUB_KEY" echo "🚀 Deploying SNAPSHOT to Sonatype Central Portal..." if [ -n "$GPG_PASSPHRASE" ] && [ -n "$GPG_PUB_KEY" ]; then mvn -B -ntp --show-version \ diff --git a/.github/workflows/deploy-central-snapshot.yml b/.github/workflows/deploy-central-snapshot.yml index 087083f14..74db65260 100644 --- a/.github/workflows/deploy-central-snapshot.yml +++ b/.github/workflows/deploy-central-snapshot.yml @@ -80,7 +80,7 @@ jobs: name: Deploy Snapshot to Central Portal runs-on: ubuntu-latest needs: [verify-snapshot, build] - if: needs.verify-snapshot.outputs.is_snapshot == 'true' + if: needs.verify-snapshot.outputs.is_snapshot == 'true' && needs.build.result == 'success' environment: maven-central steps: - name: Checkout @@ -104,6 +104,7 @@ jobs: password: ${{ secrets.CENTRAL_REPOSITORY_PASS }} - name: Summary + if: success() run: | echo "## 🚀 Snapshot Deployed to Central Portal" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY diff --git a/pom.xml b/pom.xml index 4db0675e2..72c22713f 100644 --- a/pom.xml +++ b/pom.xml @@ -367,9 +367,7 @@ org.sonatype.central central-publishing-maven-plugin - central - USER_MANAGED - true + true From 959d010946305e66d3b1e1ee5b134e463dbdd25a Mon Sep 17 00:00:00 2001 From: vibhutikumar <160819926+vibhutikumar07@users.noreply.github.com> Date: Wed, 29 Jul 2026 18:08:50 +0530 Subject: [PATCH 10/16] Removed run on push --- .github/workflows/deploy-central-snapshot.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/deploy-central-snapshot.yml b/.github/workflows/deploy-central-snapshot.yml index 74db65260..6b6c6b1d7 100644 --- a/.github/workflows/deploy-central-snapshot.yml +++ b/.github/workflows/deploy-central-snapshot.yml @@ -16,10 +16,6 @@ on: - 'true' - 'false' - # Auto-trigger on push to this feature branch - push: - branches: - - snapshot_maven permissions: contents: read From 91d5984de2261bafb60c53c04cf1cb62e6dc2353 Mon Sep 17 00:00:00 2001 From: vibhutikumar <160819926+vibhutikumar07@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:13:34 +0530 Subject: [PATCH 11/16] Fix snapshot deploy: disable central-publishing-plugin extension in deploy-central-snapshot profile With extensions=true in pluginManagement, central-publishing-maven-plugin replaces the entire deploy lifecycle, making maven-deploy-plugin unreachable. Setting extensions=false within this profile restores standard deploy behavior so maven-deploy-plugin can push directly to the Central Snapshots URL. --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 72c22713f..f00694673 100644 --- a/pom.xml +++ b/pom.xml @@ -366,6 +366,7 @@ org.sonatype.central central-publishing-maven-plugin + false true From d9d03274a5868e519b355801c7beab3907664e3d Mon Sep 17 00:00:00 2001 From: vibhutikumar <160819926+vibhutikumar07@users.noreply.github.com> Date: Fri, 31 Jul 2026 16:04:24 +0530 Subject: [PATCH 12/16] Add push trigger for RBSDMS-NoCqnSnapshot-feature branch --- .github/workflows/deploy-central-snapshot.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/deploy-central-snapshot.yml b/.github/workflows/deploy-central-snapshot.yml index 6b6c6b1d7..606a33d7c 100644 --- a/.github/workflows/deploy-central-snapshot.yml +++ b/.github/workflows/deploy-central-snapshot.yml @@ -16,6 +16,11 @@ on: - 'true' - 'false' + # Auto-trigger on push to testing branch + push: + branches: + - RBSDMS-NoCqnSnapshot-feature + permissions: contents: read From 9eea6860102318b3dc7da1266c45738df1340ab8 Mon Sep 17 00:00:00 2001 From: vibhutikumar <160819926+vibhutikumar07@users.noreply.github.com> Date: Fri, 31 Jul 2026 16:05:13 +0530 Subject: [PATCH 13/16] updated snapshot version in pom to 1.9.3-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f00694673..c319e0129 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ - 1.9.2 + 1.9.3-SNAPSHOT 17 ${java.version} ${java.version} From 89ecf74cc42d540573afeda65148f2b82e23c0c0 Mon Sep 17 00:00:00 2001 From: PujaDeshmukh17 Date: Wed, 5 Aug 2026 17:18:46 +0530 Subject: [PATCH 14/16] Changes for no cqn error --- .../SDMReadAttachmentsHandler.java | 314 ++++++++++++++++++ .../SDMUpdateAttachmentsHandler.java | 11 +- .../com/sap/cds/sdm/utilities/SDMUtils.java | 13 + .../sap/cds/sdm/utilities/SDMUtilsTest.java | 55 +++ 4 files changed, 387 insertions(+), 6 deletions(-) diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMReadAttachmentsHandler.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMReadAttachmentsHandler.java index d8b543292..241e28531 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMReadAttachmentsHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMReadAttachmentsHandler.java @@ -1,10 +1,14 @@ package com.sap.cds.sdm.handler.applicationservice; +import com.sap.cds.CdsData; +import com.sap.cds.Result; import com.sap.cds.ql.CQL; import com.sap.cds.ql.Predicate; import com.sap.cds.ql.cqn.CqnSelect; import com.sap.cds.ql.cqn.Modifier; +import com.sap.cds.reflect.CdsAnnotation; import com.sap.cds.reflect.CdsAssociationType; +import com.sap.cds.reflect.CdsElement; import com.sap.cds.reflect.CdsElementDefinition; import com.sap.cds.reflect.CdsEntity; import com.sap.cds.reflect.CdsModel; @@ -26,12 +30,14 @@ import com.sap.cds.services.cds.CdsReadEventContext; import com.sap.cds.services.draft.Drafts; import com.sap.cds.services.handler.EventHandler; +import com.sap.cds.services.handler.annotations.After; import com.sap.cds.services.handler.annotations.Before; import com.sap.cds.services.handler.annotations.HandlerOrder; import com.sap.cds.services.handler.annotations.ServiceName; import com.sap.cds.services.persistence.PersistenceService; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -405,4 +411,312 @@ private RepoValue checkRepositoryTypeWithFallback( return null; } } + + /** + * After reading a parent entity, counts its attachments per composition facet and sets the + * corresponding virtual uploadable flag (e.g. {@code isAttachmentsUploadable}) in each result + * row. Values are computed at read time so no flag is ever written to the consumer's database. + */ + @After + @HandlerOrder(HandlerOrder.LATE) + public void populateUploadableFlags(CdsReadEventContext context, List data) { + if (data == null || data.isEmpty()) return; + + CdsEntity target = context.getTarget(); + logger.info( + "populateUploadableFlags: entity={} rows={}", target.getQualifiedName(), data.size()); + + List facets = findFacetsWithMaxCount(target); + if (!facets.isEmpty()) { + logger.debug( + "populateUploadableFlags Path1: entity={} facets={}", + target.getQualifiedName(), + facets.size()); + + String keyField = + target + .elements() + .filter(CdsElement::isKey) + .filter(e -> !"IsActiveEntity".equals(e.getName())) + .map(CdsElement::getName) + .findFirst() + .orElse(null); + if (keyField == null) return; + + long keyFieldCount = + target + .elements() + .filter(CdsElement::isKey) + .filter(e -> !"IsActiveEntity".equals(e.getName())) + .count(); + if (keyFieldCount > 1) { + logger.warn( + "populateUploadableFlags Path1: entity={} has {} key fields; only '{}' is used for parentId lookup", + target.getQualifiedName(), + keyFieldCount, + keyField); + } + + CdsModel model = context.getModel(); + // Cache keyed by "facetName|parentId|isDraft" to avoid one DB query per row per facet. + Map uploadableCache = new HashMap<>(); + + // Non-draft entities have no IsActiveEntity; treat every row as active (isDraft=false). + boolean entityHasDraftSupport = target.findElement("IsActiveEntity").isPresent(); + + for (CdsData row : data) { + // Determine draft state per row — a single result set can mix active and draft records. + // On a non-draft entity IsActiveEntity is absent; default to false (active). + boolean rowIsDraft = + entityHasDraftSupport && Boolean.FALSE.equals(row.get("IsActiveEntity")); + Object keyVal = row.get(keyField); + if (keyVal == null) { + logger.debug("populateUploadableFlags Path1: skipping row with null keyVal"); + continue; + } + String parentId = keyVal.toString(); + + for (FacetInfo facet : facets) { + String attachmentEntityBase = target.getQualifiedName() + "." + facet.facetName; + CdsEntity attachmentEntity = + resolveAttachmentEntityForCount(model, attachmentEntityBase, rowIsDraft); + if (attachmentEntity == null) { + logger.debug( + "populateUploadableFlags Path1: entity not found, skipping facet={}", + facet.facetName); + continue; + } + + String upIdKey = SDMUtils.getUpIdKey(attachmentEntity); + if (upIdKey.isEmpty()) continue; + + String cacheKey = facet.facetName + "|" + parentId + "|" + rowIsDraft; + boolean isUploadable = + uploadableCache.computeIfAbsent( + cacheKey, + k -> + dbQuery + .getAttachmentsForUPID( + attachmentEntity, persistenceService, parentId, upIdKey) + .rowCount() + < facet.maxCount); + logger.debug( + "Path1: entity={} parentId={} facet={} uploadable={}", + target.getQualifiedName(), + parentId, + facet.facetName, + isUploadable); + row.put(facet.virtualFieldName, isUploadable); + } + } + return; + } + + logger.info( + "populateUploadableFlags Path2: entity={} checking for up_ expansion", + target.getQualifiedName()); + populateUploadableFlagsViaUp(context, target, data); + } + + /** + * Populates {@code up_.isXxxUploadable} on attachment entity result rows that carry an expanded + * {@code up_} navigation property. Called when the target entity is an attachment (not a parent) + * and Fiori requested {@code $expand=up_} to evaluate the Insert button state. + */ + private void populateUploadableFlagsViaUp( + CdsReadEventContext context, CdsEntity attachmentEntity, List data) { + String entityQName = attachmentEntity.getQualifiedName(); + boolean hasUpData = data.stream().anyMatch(row -> row.get("up_") != null); + logger.info( + "populateUploadableFlagsViaUp: entity={} rows={} hasUpData={}", + entityQName, + data.size(), + hasUpData); + if (!hasUpData) return; + + // CAP names draft sibling tables with a "_drafts" suffix — a stable framework convention. + boolean isDraft = entityQName.endsWith("_drafts"); + logger.debug("populateUploadableFlagsViaUp: isDraft={}", isDraft); + String baseEntityName = + isDraft ? entityQName.substring(0, entityQName.length() - 7) : entityQName; + + int lastDot = baseEntityName.lastIndexOf('.'); + if (lastDot < 0) { + logger.debug( + "populateUploadableFlagsViaUp: no dot in entity name={}, skipping", baseEntityName); + return; + } + String facetName = baseEntityName.substring(lastDot + 1); + String parentBaseEntityName = baseEntityName.substring(0, lastDot); + logger.info( + "populateUploadableFlagsViaUp: facetName={} parentEntity={}", + facetName, + parentBaseEntityName); + + CdsModel model = context.getModel(); + CdsEntity baseParentEntity = model.findEntity(parentBaseEntityName).orElse(null); + if (baseParentEntity == null) { + logger.debug( + "populateUploadableFlagsViaUp: parent entity not found={}", parentBaseEntityName); + return; + } + + Optional> maxCountAnnotation = + baseParentEntity + .compositions() + .filter(c -> facetName.equals(c.getName())) + .findFirst() + .flatMap(c -> c.findAnnotation(SDMConstants.ATTACHMENT_MAXCOUNT)); + if (!maxCountAnnotation.isPresent()) { + logger.info( + "populateUploadableFlagsViaUp: no maxCount for facet={} on entity={}", + facetName, + parentBaseEntityName); + return; + } + + long maxCount; + try { + maxCount = Long.parseLong(String.valueOf(maxCountAnnotation.get().getValue())); + } catch (NumberFormatException e) { + logger.debug( + "populateUploadableFlagsViaUp: invalid maxCount value={} for facet={}", + maxCountAnnotation.get().getValue(), + facetName); + return; + } + if (maxCount <= 0) { + logger.debug( + "populateUploadableFlagsViaUp: maxCount={} is non-positive for facet={}, skipping", + maxCount, + facetName); + return; + } + logger.debug("populateUploadableFlagsViaUp: maxCount={} facet={}", maxCount, facetName); + + String virtualFieldName = toVirtualFieldName(facetName); + logger.debug("populateUploadableFlagsViaUp: virtualField={}", virtualFieldName); + + String upIdKey = SDMUtils.getUpIdKey(attachmentEntity); + logger.debug("populateUploadableFlagsViaUp: upIdKey={}", upIdKey); + if (upIdKey.isEmpty()) return; + + Map uploadableCache = new HashMap<>(); + for (CdsData row : data) { + Object upDataObj = row.get("up_"); + if (!(upDataObj instanceof Map)) continue; + + @SuppressWarnings("unchecked") + Map upMap = (Map) upDataObj; + + Object parentIdObj = row.get(upIdKey); + if (parentIdObj == null) continue; + String parentId = parentIdObj.toString(); + + boolean isUploadable = + uploadableCache.computeIfAbsent( + parentId, + id -> { + Result countResult = + dbQuery.getAttachmentsForUPID( + attachmentEntity, persistenceService, id, upIdKey); + return countResult.rowCount() < maxCount; + }); + + logger.debug( + "up_ expansion: entity={} parentId={} facet={} virtualField={} uploadable={}", + entityQName, + parentId, + facetName, + virtualFieldName, + isUploadable); + // Written into the up_ map, not into row: Fiori evaluates the Insert button state from + // up_.isXxxUploadable via the $expand=up_ response, not from the attachment row itself. + upMap.put(virtualFieldName, isUploadable); + } + } + + private List findFacetsWithMaxCount(CdsEntity target) { + List result = new ArrayList<>(); + List compositions = target.compositions().collect(Collectors.toList()); + for (CdsElementDefinition composition : compositions) { + String facetName = composition.getName(); + logger.debug("findFacetsWithMaxCount: checking composition={}", facetName); + Optional> maxCountAnnotation = + composition.findAnnotation(SDMConstants.ATTACHMENT_MAXCOUNT); + if (!maxCountAnnotation.isPresent()) { + logger.debug( + "findFacetsWithMaxCount: no maxCount annotation for composition={}", facetName); + continue; + } + + long maxCount; + try { + maxCount = Long.parseLong(String.valueOf(maxCountAnnotation.get().getValue())); + } catch (NumberFormatException e) { + logger.debug( + "findFacetsWithMaxCount: invalid maxCount value for composition={}", facetName); + continue; + } + if (maxCount <= 0) { + logger.debug( + "findFacetsWithMaxCount: maxCount={} is non-positive for composition={}, skipping", + maxCount, + facetName); + continue; + } + + String virtualFieldName = toVirtualFieldName(facetName); + logger.debug( + "findFacetsWithMaxCount: facet={} virtualField={} maxCount={}", + facetName, + virtualFieldName, + maxCount); + result.add(new FacetInfo(facetName, virtualFieldName, maxCount)); + } + logger.debug("findFacetsWithMaxCount: found {} facet(s) with maxCount", result.size()); + return result; + } + + private CdsEntity resolveAttachmentEntityForCount( + CdsModel model, String baseEntityName, boolean isDraft) { + logger.debug("resolveAttachmentEntityForCount: base={} isDraft={}", baseEntityName, isDraft); + if (isDraft) { + Optional draftOpt = model.findEntity(baseEntityName + "_drafts"); + if (draftOpt.isPresent()) { + logger.debug( + "resolveAttachmentEntityForCount: resolved to draft entity={}", + baseEntityName + "_drafts"); + return draftOpt.get(); + } + logger.warn( + "resolveAttachmentEntityForCount: _drafts entity not found for '{}', falling back to active entity", + baseEntityName); + } + CdsEntity active = model.findEntity(baseEntityName).orElse(null); + logger.debug( + "resolveAttachmentEntityForCount: resolved to active entity={} found={}", + baseEntityName, + active != null); + return active; + } + + private static String toVirtualFieldName(String facetName) { + return "is" + + Character.toUpperCase(facetName.charAt(0)) + + facetName.substring(1) + + "Uploadable"; + } + + private static final class FacetInfo { + final String facetName; + final String virtualFieldName; + final long maxCount; + + FacetInfo(String facetName, String virtualFieldName, long maxCount) { + this.facetName = facetName; + this.virtualFieldName = virtualFieldName; + this.maxCount = maxCount; + } + } } diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandler.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandler.java index 65126256d..48279f5db 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandler.java @@ -683,12 +683,11 @@ private void cleanupReadonlyContextsForAttachments( targetEntity); } } - // Fallback: recursively remove SDM_READONLY_CONTEXT from any nested structure - // that fetchAttachments failed to resolve (e.g. deeply nested compositions) - logger.info( - "[SDM] UPDATE: Running recursive fallback to remove SDM_READONLY_CONTEXT from entity '{}'. " - + "Any WARN entries above indicate compositions where fetchAttachments could not resolve attachments.", - targetEntity); + // Use CdsDataProcessor to mirror the exact traversal path used by preserveReadonlyFields. + // This handles cases where CdsData stores composition data internally (e.g. during + // draftActivate) in a way that plain Map.values() iteration cannot reach. + SDMUtils.removeReadonlyFields(context.getTarget(), List.of(CdsData.create(entityData))); + // Plain-map recursive fallback as secondary safety net for any remaining entries. removeReadonlyContextRecursively(entityData); } diff --git a/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java b/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java index 66b68944c..42e388447 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java +++ b/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java @@ -278,6 +278,19 @@ public static void preserveReadonlyFields(CdsEntity target, List data) CdsDataProcessor.create().addValidator(mediaContentFilter, validator).process(data, target); } + public static void removeReadonlyFields(CdsEntity target, List data) { + CdsDataProcessor.Filter mediaContentFilter = + (path, element, type) -> element.findAnnotation("Core.MediaType").isPresent(); + + CdsDataProcessor.Validator validator = + (path, element, value) -> { + Map values = path.target().values(); + values.remove(SDM_READONLY_CONTEXT); + }; + + CdsDataProcessor.create().addValidator(mediaContentFilter, validator).process(data, target); + } + public static String getErrorMessage(String errorKey) { ErrorMessageKey errorMessageKey = new ErrorMessageKey(); errorMessageKey.setKey(errorKey); diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java index 8cc8484b3..28d2859ed 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java @@ -1704,6 +1704,61 @@ void testGetUpdatedSecondaryProperties_WithBooleanValues_ConvertsToString() { assertEquals("false", result.get("Property 2")); } + // --- Tests for removeReadonlyFields --- + + @Test + @SuppressWarnings("unchecked") + void testRemoveReadonlyFields_removesSDMReadonlyContextWhenPresent() { + // Arrange: an attachment CdsData with SDM_READONLY_CONTEXT set + // (simulating preserveReadonlyFields) + Map attachment = new HashMap<>(); + attachment.put("ID", "att1"); + attachment.put("uploadStatus", "InProgress"); + attachment.put("content", new byte[0]); + attachment.put(SDMConstants.SDM_READONLY_CONTEXT, Map.of("uploadStatus", "InProgress")); + + CdsData data = CdsData.create(attachment); + + CdsElement contentElement = mock(CdsElement.class); + CdsAnnotation mediaTypeAnnotation = mock(CdsAnnotation.class); + when(contentElement.getName()).thenReturn("content"); + when(contentElement.findAnnotation("Core.MediaType")) + .thenReturn(Optional.of(mediaTypeAnnotation)); + + // CdsDataProcessor needs elements() to find the annotated element; return a stream with it + when(mockEntity.elements()).thenReturn(Stream.of(contentElement)); + + // Act + SDMUtils.removeReadonlyFields(mockEntity, List.of(data)); + + // Assert: SDM_READONLY_CONTEXT is removed + assertFalse( + data.containsKey(SDMConstants.SDM_READONLY_CONTEXT), + "removeReadonlyFields should remove SDM_READONLY_CONTEXT from attachment"); + } + + @Test + @SuppressWarnings("unchecked") + void testRemoveReadonlyFields_noopWhenSDMReadonlyContextAbsent() { + Map attachment = new HashMap<>(); + attachment.put("ID", "att2"); + attachment.put("uploadStatus", "Success"); + + CdsData data = CdsData.create(attachment); + + CdsElement contentElement = mock(CdsElement.class); + CdsAnnotation mediaTypeAnnotation = mock(CdsAnnotation.class); + when(contentElement.getName()).thenReturn("content"); + when(contentElement.findAnnotation("Core.MediaType")) + .thenReturn(Optional.of(mediaTypeAnnotation)); + when(mockEntity.elements()).thenReturn(Stream.of(contentElement)); + + // Should not throw and data is unchanged + SDMUtils.removeReadonlyFields(mockEntity, List.of(data)); + + assertFalse(data.containsKey(SDMConstants.SDM_READONLY_CONTEXT)); + } + @Test void testGetUpdatedSecondaryProperties_WithEmptyPropertiesInDB_AddsAll() { Map attachment = new HashMap<>(); From 2150f035d54a4b2e787572fc686281253e358065 Mon Sep 17 00:00:00 2001 From: PujaDeshmukh17 Date: Thu, 6 Aug 2026 17:13:39 +0530 Subject: [PATCH 15/16] Create attachment --- .../SDMCreateAttachmentsHandler.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandler.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandler.java index ca12ecdbf..2c1c193a1 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandler.java @@ -678,12 +678,11 @@ private void cleanupReadonlyContextsForAttachments( targetEntity); } } - // Fallback: recursively remove SDM_READONLY_CONTEXT from any nested structure - // that fetchAttachments failed to resolve (e.g. deeply nested compositions) - logger.info( - "[SDM] CREATE: Running recursive fallback to remove SDM_READONLY_CONTEXT from entity '{}'. " - + "Any WARN entries above indicate compositions where fetchAttachments could not resolve attachments.", - targetEntity); + // Use CdsDataProcessor to mirror the exact traversal path used by preserveReadonlyFields. + // This handles cases where CdsData stores composition data internally (e.g. during + // draftActivate) in a way that plain Map.values() iteration cannot reach. + SDMUtils.removeReadonlyFields(context.getTarget(), List.of(CdsData.create(entityData))); + // Plain-map recursive fallback as secondary safety net for any remaining entries. removeReadonlyContextRecursively(entityData); } From 86af5d86ea05c46418611d5c8a7d54f1dd2f4654 Mon Sep 17 00:00:00 2001 From: PujaDeshmukh17 Date: Thu, 6 Aug 2026 18:04:22 +0530 Subject: [PATCH 16/16] Adding more logs for debugging --- .../SDMCreateAttachmentsHandler.java | 50 +++++++++++++++++-- .../SDMUpdateAttachmentsHandler.java | 14 ++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandler.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandler.java index 2c1c193a1..a89ed7ac4 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandler.java @@ -73,24 +73,41 @@ public void updateActiveEntitySdmMetadata(CdsCreateEventContext _context) { } private void handleUpdateActiveEntitySdmMetadata() { + logger.debug( + "[CREATE] handleUpdateActiveEntitySdmMetadata: checking ThreadLocal for SDM metadata"); Map metadata = SDMAttachmentsServiceHandler.SDM_METADATA_THREADLOCAL.get(); if (metadata == null) { + logger.debug( + "[CREATE] handleUpdateActiveEntitySdmMetadata: no ThreadLocal metadata found, skipping"); return; } try { SDMAttachmentsServiceHandler.SDM_METADATA_THREADLOCAL.remove(); + logger.debug( + "[CREATE] handleUpdateActiveEntitySdmMetadata: ThreadLocal metadata keys: {}", + metadata.keySet()); com.sap.cds.reflect.CdsEntity attachmentEntity = (com.sap.cds.reflect.CdsEntity) metadata.get("attachmentEntity"); if (attachmentEntity == null) { logger.warn("No attachmentEntity in ThreadLocal metadata, skipping post-INSERT update"); return; } + logger.debug( + "[CREATE] handleUpdateActiveEntitySdmMetadata: attachmentEntity={}", + attachmentEntity.getQualifiedName()); CmisDocument cmisDocument = new CmisDocument(); cmisDocument.setAttachmentId((String) metadata.get("attachmentId")); cmisDocument.setObjectId((String) metadata.get("objectId")); cmisDocument.setFolderId((String) metadata.get("folderId")); cmisDocument.setMimeType((String) metadata.get("mimeType")); cmisDocument.setUploadStatus((String) metadata.get("uploadStatus")); + logger.debug( + "[CREATE] handleUpdateActiveEntitySdmMetadata: cmisDocument attachmentId={} objectId={} folderId={} mimeType={} uploadStatus={}", + cmisDocument.getAttachmentId(), + cmisDocument.getObjectId(), + cmisDocument.getFolderId(), + cmisDocument.getMimeType(), + cmisDocument.getUploadStatus()); logger.info( "Post-INSERT: Updating active entity attachment {} with objectId {}", cmisDocument.getAttachmentId(), @@ -201,9 +218,12 @@ public void processAfter(CdsCreateEventContext context, List data) { public void preserveUploadStatus(CdsCreateEventContext context, List data) { // Preserve uploadStatus before CDS removes readonly fields logger.debug( - "Preserving readonly fields (uploadStatus) for entity: {} before CDS capability check", - context.getTarget().getQualifiedName()); + "[CREATE] preserveUploadStatus: entity={} dataSize={}", + context.getTarget().getQualifiedName(), + data.size()); SDMUtils.preserveReadonlyFields(context.getTarget(), data); + logger.debug( + "[CREATE] preserveUploadStatus: SDM_READONLY_CONTEXT set on attachment maps via CdsDataProcessor"); } public void updateName( @@ -231,9 +251,18 @@ public void updateName( Optional attachmentEntity = context.getModel().findEntity(attachmentCompositionDefinition); + logger.debug( + "[CREATE] updateName: processing composition={} entityFound={}", + attachmentCompositionName, + attachmentEntity.isPresent()); isError = AttachmentsHandlerUtils.validateFileNames( context, data, attachmentCompositionName, contextInfo, attachmentEntity); + if (isError) { + logger.debug( + "[CREATE] updateName: filename validation failed for composition={}, skipping SDM update", + attachmentCompositionName); + } if (!isError) { List fileNameWithRestrictedCharacters = new ArrayList<>(); List duplicateFileNameList = new ArrayList<>(); @@ -305,6 +334,10 @@ private void processEntity( List uploadInProgressFiles = new ArrayList<>(); if (attachments != null) { + logger.debug( + "[CREATE] processEntity: composition={} attachmentCount={}", + attachmentCompositionName, + attachments.size()); for (Map attachment : attachments) { processAttachment( context, @@ -325,6 +358,10 @@ private void processEntity( // Throw exception if any files failed scan or upload in progress String errorMessage = buildErrorMessage(scanFailedFiles, uploadInProgressFiles); if (!errorMessage.isEmpty()) { + logger.debug( + "[CREATE] processEntity: blocking — scanFailed={} uploadInProgress={}", + scanFailedFiles, + uploadInProgressFiles); throw new ServiceException(errorMessage); } @@ -482,7 +519,12 @@ private void updateAndSendToSDM( dbQuery.getPropertiesForID( attachmentEntity.get(), persistenceService, id, secondaryTypeProperties); - logger.debug("Processing attachment creation - ID: {}, objectId: {}", id, objectId); + logger.debug( + "[CREATE] updateAndSendToSDM: ID={} objectId={} secondaryTypeProperties={} propertiesInDB={}", + id, + objectId, + secondaryTypeProperties.keySet(), + propertiesInDB.keySet()); Map updatedSecondaryProperties = SDMUtils.getUpdatedSecondaryProperties( @@ -504,6 +546,8 @@ private void updateAndSendToSDM( updatedSecondaryProperties, false); + logger.debug( + "[CREATE] updateAndSendToSDM: updatedSecondaryProperties={}", updatedSecondaryProperties); logger.debug( "Creating attachment in SDM - ID: {}, fileName: {}, properties count: {}", id, diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandler.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandler.java index 48279f5db..5751f8eb4 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandler.java @@ -181,9 +181,18 @@ public void updateName( if (context.getModel() != null) { attachmentEntity = context.getModel().findEntity(attachmentCompositionDefinition); } + logger.debug( + "[UPDATE] updateName: processing composition={} entityFound={}", + attachmentCompositionName, + attachmentEntity.isPresent()); isError = AttachmentsHandlerUtils.validateFileNames( context, data, attachmentCompositionName, contextInfo, attachmentEntity); + if (isError) { + logger.debug( + "[UPDATE] updateName: filename validation failed for composition={}, skipping rename", + attachmentCompositionName); + } if (!isError) { renameDocument( attachmentEntity, @@ -494,6 +503,11 @@ private Map prepareUpdatedProperties( AttachmentsHandlerUtils.updateDescriptionProperty( null, descriptionInRequest, descriptionInDB, updatedSecondaryProperties, true); + logger.debug( + "[UPDATE] prepareUpdatedProperties: secondaryTypeProperties={} propertiesInDB={} updatedSecondaryProperties={}", + secondaryTypeProperties.keySet(), + propertiesInDB.keySet(), + updatedSecondaryProperties); return updatedSecondaryProperties; }