From 7b98188d0c70150069b8171beaf51379884e1be6 Mon Sep 17 00:00:00 2001 From: PujaDeshmukh17 Date: Wed, 29 Jul 2026 16:27:09 +0530 Subject: [PATCH 01/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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}