From 1f5529d25d16ee6348c2fa5678cb21de5ac2ed0e Mon Sep 17 00:00:00 2001 From: "Yashmeet ." Date: Wed, 11 Feb 2026 16:18:57 +0530 Subject: [PATCH 01/10] bug fix to display correct objectPage name in Error messages --- pom.xml | 2 +- .../helper/AttachmentsHandlerUtils.java | 205 ++++++++++++++++-- 2 files changed, 189 insertions(+), 18 deletions(-) diff --git a/pom.xml b/pom.xml index 13baf0765..a71a54c5b 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ - 1.7.1-SNAPSHOT + 1.0.0-RC1 17 ${java.version} ${java.version} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java index d9997cba9..98f00321a 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java @@ -405,7 +405,7 @@ public static Map> getAttachmentCompositionDetails( // Get parent titles Map parentTitles = - getAttachmentParentTitles(targetEntity, entityData, compositionPathMapping); + getAttachmentParentTitles(model, targetEntity, entityData, compositionPathMapping); // Combine into comprehensive details for (Map.Entry entry : compositionPathMapping.entrySet()) { @@ -432,6 +432,7 @@ public static Map> getAttachmentCompositionDetails( * composition. It handles both direct attachments at the root level and nested attachments within * composed entities. * + * @param model the CDS model containing entity definitions and relationships * @param targetEntity the qualified name of the target entity (e.g., "AdminService.Books") * @param entity the entity data structure containing potential attachment information * @param compositionPathMapping the mapping of attachment composition paths obtained from @@ -440,7 +441,10 @@ public static Map> getAttachmentCompositionDetails( * titles, or an empty map if no attachments are found */ public static Map getAttachmentParentTitles( - String targetEntity, Map entity, Map compositionPathMapping) { + CdsModel model, + String targetEntity, + Map entity, + Map compositionPathMapping) { Map parentTitles = new HashMap<>(); String[] targetEntityPath = targetEntity.split("\\."); @@ -449,7 +453,8 @@ public static Map getAttachmentParentTitles( for (Map.Entry compositionEntry : compositionPathMapping.entrySet()) { String compositionPath = compositionEntry.getValue(); - String parentTitle = findParentTitle(wrappedEntity, compositionPath, entityName); + String parentTitle = + findParentTitle(model, wrappedEntity, compositionPath, entityName, targetEntity); if (parentTitle != null) { parentTitles.put(compositionPath, parentTitle); } @@ -461,15 +466,21 @@ public static Map getAttachmentParentTitles( /** * Finds the parent title for a given attachment composition path. * + * @param model the CDS model containing entity definitions and relationships * @param entity the wrapped entity data structure * @param compositionPath the composition path (e.g., "AdminService.chapters123.attachments" or * "AdminService.Books.references") * @param rootEntityName the name of the root entity + * @param targetEntity the qualified name of the target entity * @return the title of the parent entity containing the attachment composition, or null if not * found */ private static String findParentTitle( - Map entity, String compositionPath, String rootEntityName) { + CdsModel model, + Map entity, + String compositionPath, + String rootEntityName, + String targetEntity) { try { String[] pathParts = compositionPath.split("\\."); @@ -479,7 +490,7 @@ private static String findParentTitle( // Check if this is a direct composition (entity matches root entity) if (entityPart.equalsIgnoreCase(rootEntityName)) { // Direct attachment at root level (e.g., "AdminService.Books.references") - return extractTitleFromEntity(entity.get(rootEntityName)); + return extractTitleFromEntity(model, targetEntity, entity.get(rootEntityName)); } else { // Nested attachment (e.g., "AdminService.chapters123.attachments") // Navigate to the parent entity @@ -494,7 +505,10 @@ private static String findParentTitle( List> parentList = (List>) parentCollection; if (!parentList.isEmpty()) { // Get title from the first item in the collection - return extractTitleFromEntity(parentList.get(0)); + // For nested entities, try to determine the entity type from the composition path + String nestedEntityName = + determineNestedEntityName(model, targetEntity, entityPart); + return extractTitleFromEntity(model, nestedEntityName, parentList.get(0)); } } } @@ -508,39 +522,196 @@ private static String findParentTitle( } /** - * Extracts the title field from an entity object, with fallback options. + * Determines the fully qualified entity name for a nested composition. + * + * @param model the CDS model + * @param parentEntityName the parent entity name + * @param compositionName the composition property name + * @return the fully qualified nested entity name, or null if not found + */ + private static String determineNestedEntityName( + CdsModel model, String parentEntityName, String compositionName) { + try { + Optional parentEntity = model.findEntity(parentEntityName); + if (parentEntity.isPresent()) { + Optional composition = + parentEntity.get().findElement(compositionName); + if (composition.isPresent() && composition.get().getType().isAssociation()) { + CdsAssociationType associationType = (CdsAssociationType) composition.get().getType(); + return associationType.getTarget().getQualifiedName(); + } + } + } catch (Exception e) { + logger.warn("Error determining nested entity name for composition: " + compositionName, e); + } + return null; + } + + /** + * Extracts the title field from an entity object using CDS metadata annotations. + * + *

This method attempts to determine the entity title using the following approach: + * + *

    + *
  1. UI.HeaderInfo.Title annotation - The official SAP Fiori recommended method for + * defining object page titles. This is the only documented standard approach. + *
  2. Common.SemanticKey annotation - Uses the first semantic key field as a fallback. + * While SemanticKey is documented for entity identification, its use for titles is a + * pragmatic fallback, not an official standard. + *
* + *

Important: If neither annotation is defined, this method returns an empty string. + * Always define {@code UI.HeaderInfo.Title} annotation in your CDS models for proper object page + * title display. Example: + * + *

{@code
+   * annotate Books with @(UI.HeaderInfo: {
+   *   Title: { Value: title }
+   * });
+   * }
+ * + * @param model the CDS model containing entity definitions and annotations + * @param entityName the qualified name of the entity (e.g., "AdminService.Books") * @param entityObj the entity object to extract title from - * @return the title string, or a fallback identifier, or null if not found + * @return the title string from annotations, or empty string if not found */ - private static String extractTitleFromEntity(Object entityObj) { + private static String extractTitleFromEntity( + CdsModel model, String entityName, Object entityObj) { if (!(entityObj instanceof Map)) { - return null; + return ""; } @SuppressWarnings("unchecked") Map entityMap = (Map) entityObj; - // Priority order: title -> name -> ID -> first non-null string value - String[] titleFields = {"title", "name", "ID", "id"}; - - for (String field : titleFields) { - Object value = entityMap.get(field); + // 1. Try to get title field from UI.HeaderInfo.Title annotation + String titleFieldFromAnnotation = getTitleFieldFromAnnotation(model, entityName); + if (titleFieldFromAnnotation != null) { + Object value = getNestedValue(entityMap, titleFieldFromAnnotation); if (value != null && value instanceof String && !((String) value).trim().isEmpty()) { return (String) value; } } - // Fallback: find any string value - for (Object value : entityMap.values()) { + // 2. Try to get title field from Common.SemanticKey annotation + String titleFieldFromSemanticKey = getSemanticKeyField(model, entityName); + if (titleFieldFromSemanticKey != null) { + Object value = entityMap.get(titleFieldFromSemanticKey); if (value != null && value instanceof String && !((String) value).trim().isEmpty()) { return (String) value; } } + // Return empty string if no annotation-based title is found + return ""; + } + + /** + * Extracts the title field name from UI.HeaderInfo.Title annotation. + * + * @param model the CDS model + * @param entityName the qualified entity name + * @return the field name configured as title, or null if not found + */ + private static String getTitleFieldFromAnnotation(CdsModel model, String entityName) { + if (model == null || entityName == null) { + return null; + } + + try { + Optional entityOpt = model.findEntity(entityName); + if (entityOpt.isPresent()) { + CdsEntity entity = entityOpt.get(); + Optional> headerInfoOpt = + entity.findAnnotation("UI.HeaderInfo"); + + if (headerInfoOpt.isPresent()) { + Object headerInfo = headerInfoOpt.get().getValue(); + if (headerInfo instanceof Map) { + @SuppressWarnings("unchecked") + Map headerMap = (Map) headerInfo; + Object titleObj = headerMap.get("Title"); + + if (titleObj instanceof Map) { + @SuppressWarnings("unchecked") + Map titleMap = (Map) titleObj; + Object value = titleMap.get("Value"); + if (value != null) { + return value.toString(); + } + } + } + } + } + } catch (Exception e) { + logger.debug("Error extracting title field from UI.HeaderInfo annotation: " + e.getMessage()); + } + return null; } + /** + * Extracts the first field from Common.SemanticKey annotation. + * + * @param model the CDS model + * @param entityName the qualified entity name + * @return the first semantic key field name, or null if not found + */ + private static String getSemanticKeyField(CdsModel model, String entityName) { + if (model == null || entityName == null) { + return null; + } + + try { + Optional entityOpt = model.findEntity(entityName); + if (entityOpt.isPresent()) { + CdsEntity entity = entityOpt.get(); + Optional> semanticKeyOpt = + entity.findAnnotation("Common.SemanticKey"); + + if (semanticKeyOpt.isPresent() && semanticKeyOpt.get().getValue() instanceof List) { + @SuppressWarnings("unchecked") + List keys = (List) semanticKeyOpt.get().getValue(); + if (!keys.isEmpty()) { + return keys.get(0).toString(); + } + } + } + } catch (Exception e) { + logger.debug("Error extracting semantic key field: " + e.getMessage()); + } + + return null; + } + + /** + * Gets a nested value from a map using a path (e.g., "author.name"). + * + * @param map the map to extract value from + * @param path the path to the value (can include dots for nested access) + * @return the value at the path, or null if not found + */ + private static Object getNestedValue(Map map, String path) { + if (path == null || map == null) { + return null; + } + + String[] parts = path.split("\\."); + Object current = map; + + for (String part : parts) { + if (current instanceof Map) { + @SuppressWarnings("unchecked") + Map currentMap = (Map) current; + current = currentMap.get(part); + } else { + return null; + } + } + + return current; + } + /** * Validates file names in the provided data for various constraints including whitespace, * restricted characters, and duplicates. From 3a9103b0cbe4c867901275a99c4be16aa3402720 Mon Sep 17 00:00:00 2001 From: "Yashmeet ." Date: Sat, 14 Feb 2026 13:57:16 +0530 Subject: [PATCH 02/10] Update logic to fetch title of entity --- sdm/pom.xml | 2 +- .../helper/AttachmentsHandlerUtils.java | 215 +++++++++++++----- 2 files changed, 164 insertions(+), 53 deletions(-) diff --git a/sdm/pom.xml b/sdm/pom.xml index 5d700f777..3810df5be 100644 --- a/sdm/pom.xml +++ b/sdm/pom.xml @@ -34,7 +34,7 @@ src/test/gen 17 17 - 1.3.0 + 1.3.1 1.18.36 0.8.7 3.10.8 diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java index 98f00321a..a6d615b98 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java @@ -481,18 +481,36 @@ private static String findParentTitle( String compositionPath, String rootEntityName, String targetEntity) { + logger.info( + "findParentTitle: compositionPath={}, rootEntityName={}, targetEntity={}", + compositionPath, + rootEntityName, + targetEntity); + logger.info("findParentTitle: entity keys={}", entity.keySet()); + try { String[] pathParts = compositionPath.split("\\."); + logger.info("findParentTitle: pathParts={}", String.join(",", pathParts)); if (pathParts.length >= 3) { String entityPart = pathParts[pathParts.length - 2]; // Second to last part (entity name) + logger.info("findParentTitle: entityPart={} (second to last)", entityPart); // Check if this is a direct composition (entity matches root entity) if (entityPart.equalsIgnoreCase(rootEntityName)) { // Direct attachment at root level (e.g., "AdminService.Books.references") - return extractTitleFromEntity(model, targetEntity, entity.get(rootEntityName)); + logger.info( + "findParentTitle: Direct attachment detected, looking up entity.get({})", + rootEntityName); + Object entityData = entity.get(rootEntityName); + logger.info( + "findParentTitle: entityData type={}, isNull={}", + entityData != null ? entityData.getClass().getSimpleName() : "null", + entityData == null); + return extractTitleFromEntity(model, targetEntity, entityData); } else { // Nested attachment (e.g., "AdminService.chapters123.attachments") + logger.info("findParentTitle: Nested attachment detected"); // Navigate to the parent entity Object rootEntity = entity.get(rootEntityName); if (rootEntity instanceof Map) { @@ -518,6 +536,7 @@ private static String findParentTitle( logger.warn("Error finding parent title for composition path: " + compositionPath, e); } + logger.info("findParentTitle: Returning null"); return null; } @@ -550,71 +569,109 @@ private static String determineNestedEntityName( /** * Extracts the title field from an entity object using CDS metadata annotations. * - *

This method attempts to determine the entity title using the following approach: + *

This method extracts entity titles using @Common.Text annotation on the semantic key field, + * which is the only mechanism proven to work reliably in both Fiori UI and Java backend through + * empirical testing. + * + *

How it works: * *

    - *
  1. UI.HeaderInfo.Title annotation - The official SAP Fiori recommended method for - * defining object page titles. This is the only documented standard approach. - *
  2. Common.SemanticKey annotation - Uses the first semantic key field as a fallback. - * While SemanticKey is documented for entity identification, its use for titles is a - * pragmatic fallback, not an official standard. + *
  3. Finds the semantic key field from @Common.SemanticKey annotation + *
  4. Checks if that field has a @Common.Text annotation pointing to a title field + *
  5. Extracts and returns the value of the title field *
* - *

Important: If neither annotation is defined, this method returns an empty string. - * Always define {@code UI.HeaderInfo.Title} annotation in your CDS models for proper object page - * title display. Example: + *

Important: Define your CDS model as follows for proper title extraction: * *

{@code
-   * annotate Books with @(UI.HeaderInfo: {
-   *   Title: { Value: title }
-   * });
+   * entity Books {
+   *   key ID : UUID;
+   *   title  : String;
+   * }
+   *
+   * annotate Books with @Common.SemanticKey: [ID] {
+   *   ID @Common.Text: title;
+   * }
    * }
* + *

Note: UI.HeaderInfo.Title annotations defined in app/common.cds are NOT accessible to + * Java backend code via CDS Reflection API. They are only used by Fiori UI layer for OData + * metadata generation. + * * @param model the CDS model containing entity definitions and annotations * @param entityName the qualified name of the entity (e.g., "AdminService.Books") * @param entityObj the entity object to extract title from - * @return the title string from annotations, or empty string if not found + * @return the title string from annotations, or null if not found */ private static String extractTitleFromEntity( CdsModel model, String entityName, Object entityObj) { if (!(entityObj instanceof Map)) { - return ""; + logger.info("extractTitleFromEntity: entityObj is not a Map for entity: {}", entityName); + return null; } @SuppressWarnings("unchecked") Map entityMap = (Map) entityObj; - // 1. Try to get title field from UI.HeaderInfo.Title annotation - String titleFieldFromAnnotation = getTitleFieldFromAnnotation(model, entityName); - if (titleFieldFromAnnotation != null) { - Object value = getNestedValue(entityMap, titleFieldFromAnnotation); - if (value != null && value instanceof String && !((String) value).trim().isEmpty()) { - return (String) value; - } - } + logger.info( + "extractTitleFromEntity: Extracting title for entity: {}, data keys: {}", + entityName, + entityMap.keySet()); - // 2. Try to get title field from Common.SemanticKey annotation + // Get title field from Common.Text annotation on semantic key field + // This is proven to work in both Fiori UI and Java backend String titleFieldFromSemanticKey = getSemanticKeyField(model, entityName); + logger.info( + "extractTitleFromEntity: titleFieldFromSemanticKey = {} for entity: {}", + titleFieldFromSemanticKey, + entityName); + if (titleFieldFromSemanticKey != null) { - Object value = entityMap.get(titleFieldFromSemanticKey); - if (value != null && value instanceof String && !((String) value).trim().isEmpty()) { - return (String) value; + // Check if the semantic key field has a Common.Text annotation pointing to another field + String titleFieldFromCommonText = + getTitleFromCommonTextOnField(model, entityName, titleFieldFromSemanticKey); + logger.info( + "extractTitleFromEntity: titleFieldFromCommonText = {} for entity: {}", + titleFieldFromCommonText, + entityName); + + if (titleFieldFromCommonText != null) { + // Use the field specified by Common.Text annotation + Object value = getNestedValue(entityMap, titleFieldFromCommonText); + logger.info( + "extractTitleFromEntity: Value for Common.Text field '{}' = {}", + titleFieldFromCommonText, + value); + if (value != null && value instanceof String && !((String) value).trim().isEmpty()) { + logger.info( + "extractTitleFromEntity: Returning title from Common.Text annotation: {}", value); + return (String) value; + } } } - // Return empty string if no annotation-based title is found - return ""; + logger.info("extractTitleFromEntity: No title found for entity: {}", entityName); + // Return null if no annotation-based title is found + return null; } /** - * Extracts the title field name from UI.HeaderInfo.Title annotation. + * Extracts the title field name from @Common.Text annotation on a specific field. This mirrors + * how Fiori determines page titles when UI.HeaderInfo is not present. + * + *

Example: If field "ID" has @Common.Text: title, this returns "title" * * @param model the CDS model * @param entityName the qualified entity name - * @return the field name configured as title, or null if not found + * @param fieldName the field to check for @Common.Text annotation + * @return the field name from Common.Text annotation, or null if not found */ - private static String getTitleFieldFromAnnotation(CdsModel model, String entityName) { - if (model == null || entityName == null) { + private static String getTitleFromCommonTextOnField( + CdsModel model, String entityName, String fieldName) { + logger.info( + "getTitleFromCommonTextOnField: Checking field '{}' on entity '{}'", fieldName, entityName); + + if (model == null || entityName == null || fieldName == null) { return null; } @@ -622,31 +679,72 @@ private static String getTitleFieldFromAnnotation(CdsModel model, String entityN Optional entityOpt = model.findEntity(entityName); if (entityOpt.isPresent()) { CdsEntity entity = entityOpt.get(); - Optional> headerInfoOpt = - entity.findAnnotation("UI.HeaderInfo"); - - if (headerInfoOpt.isPresent()) { - Object headerInfo = headerInfoOpt.get().getValue(); - if (headerInfo instanceof Map) { - @SuppressWarnings("unchecked") - Map headerMap = (Map) headerInfo; - Object titleObj = headerMap.get("Title"); - if (titleObj instanceof Map) { + // Find the field element + Optional elementOpt = entity.findElement(fieldName); + if (elementOpt.isPresent()) { + com.sap.cds.reflect.CdsElement element = elementOpt.get(); + logger.info( + "getTitleFromCommonTextOnField: Found element '{}', checking for Common annotation", + fieldName); + + // Check for Common annotation (which contains Text property) + Optional> commonAnnotationOpt = + element.findAnnotation("Common"); + if (commonAnnotationOpt.isPresent()) { + Object commonValue = commonAnnotationOpt.get().getValue(); + logger.info( + "getTitleFromCommonTextOnField: Common annotation value type = {}", + commonValue != null ? commonValue.getClass().getSimpleName() : "null"); + + if (commonValue instanceof Map) { @SuppressWarnings("unchecked") - Map titleMap = (Map) titleObj; - Object value = titleMap.get("Value"); - if (value != null) { - return value.toString(); + Map commonMap = (Map) commonValue; + logger.info( + "getTitleFromCommonTextOnField: Common map keys = {}", commonMap.keySet()); + + // Get the Text property + Object textValue = commonMap.get("Text"); + logger.info("getTitleFromCommonTextOnField: Text value = {}", textValue); + + if (textValue != null) { + String result = textValue.toString(); + // Parse CDS element reference if needed + if (result.startsWith("{==") && result.endsWith("}")) { + result = result.substring(3, result.length() - 1); + } else if (result.startsWith("{") && result.endsWith("}")) { + result = result.substring(1, result.length() - 1); + } + logger.info("getTitleFromCommonTextOnField: Parsed title field = {}", result); + return result; + } + } + } else { + // Also try Common.Text directly (alternate format) + Optional> commonTextOpt = + element.findAnnotation("Common.Text"); + if (commonTextOpt.isPresent()) { + Object textValue = commonTextOpt.get().getValue(); + logger.info("getTitleFromCommonTextOnField: Common.Text value = {}", textValue); + + if (textValue != null) { + String result = textValue.toString(); + // Parse CDS element reference if needed + if (result.startsWith("{==") && result.endsWith("}")) { + result = result.substring(3, result.length() - 1); + } else if (result.startsWith("{") && result.endsWith("}")) { + result = result.substring(1, result.length() - 1); + } + logger.info("getTitleFromCommonTextOnField: Parsed title field = {}", result); + return result; } } } } } } catch (Exception e) { - logger.debug("Error extracting title field from UI.HeaderInfo annotation: " + e.getMessage()); + logger.info("getTitleFromCommonTextOnField: Error - {}", e.getMessage(), e); } - return null; } @@ -673,12 +771,24 @@ private static String getSemanticKeyField(CdsModel model, String entityName) { @SuppressWarnings("unchecked") List keys = (List) semanticKeyOpt.get().getValue(); if (!keys.isEmpty()) { - return keys.get(0).toString(); + String rawValue = keys.get(0).toString(); + logger.info("getSemanticKeyField: Raw value from annotation = {}", rawValue); + + // Parse CDS element references like {==ID} or {path} + String fieldName = rawValue; + if (rawValue.startsWith("{==") && rawValue.endsWith("}")) { + fieldName = rawValue.substring(3, rawValue.length() - 1); + } else if (rawValue.startsWith("{") && rawValue.endsWith("}")) { + fieldName = rawValue.substring(1, rawValue.length() - 1); + } + + logger.info("getSemanticKeyField: Parsed field name = {}", fieldName); + return fieldName; } } } } catch (Exception e) { - logger.debug("Error extracting semantic key field: " + e.getMessage()); + logger.info("getSemanticKeyField: Error - {}", e.getMessage(), e); } return null; @@ -995,6 +1105,7 @@ public static CmisDocument prepareCmisDocument( public static String getContextInfo(String compositionName, String parentTitle) { return String.format(SDMErrorMessages.CONTEXT_INFO_TABLE, compositionName) + String.format( - SDMErrorMessages.CONTEXT_INFO_PAGE, (parentTitle != null ? parentTitle : "Unknown")); + SDMErrorMessages.CONTEXT_INFO_PAGE, + (parentTitle != null && !parentTitle.trim().isEmpty() ? parentTitle : "Unknown")); } } From 4afb4ab4056b6afb8db8a8d3b3a3567081e095df Mon Sep 17 00:00:00 2001 From: "Yashmeet ." Date: Sat, 14 Feb 2026 14:10:30 +0530 Subject: [PATCH 03/10] sonar fix --- pom.xml | 2 +- .../helper/AttachmentsHandlerUtils.java | 164 +++++++++++------- 2 files changed, 100 insertions(+), 66 deletions(-) diff --git a/pom.xml b/pom.xml index a71a54c5b..107849b9b 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ - 1.0.0-RC1 + 3.0.0 17 ${java.version} ${java.version} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java index a6d615b98..0311a9d4d 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java @@ -677,75 +677,108 @@ private static String getTitleFromCommonTextOnField( try { Optional entityOpt = model.findEntity(entityName); - if (entityOpt.isPresent()) { - CdsEntity entity = entityOpt.get(); + if (!entityOpt.isPresent()) { + return null; + } - // Find the field element - Optional elementOpt = entity.findElement(fieldName); - if (elementOpt.isPresent()) { - com.sap.cds.reflect.CdsElement element = elementOpt.get(); - logger.info( - "getTitleFromCommonTextOnField: Found element '{}', checking for Common annotation", - fieldName); - - // Check for Common annotation (which contains Text property) - Optional> commonAnnotationOpt = - element.findAnnotation("Common"); - if (commonAnnotationOpt.isPresent()) { - Object commonValue = commonAnnotationOpt.get().getValue(); - logger.info( - "getTitleFromCommonTextOnField: Common annotation value type = {}", - commonValue != null ? commonValue.getClass().getSimpleName() : "null"); - - if (commonValue instanceof Map) { - @SuppressWarnings("unchecked") - Map commonMap = (Map) commonValue; - logger.info( - "getTitleFromCommonTextOnField: Common map keys = {}", commonMap.keySet()); - - // Get the Text property - Object textValue = commonMap.get("Text"); - logger.info("getTitleFromCommonTextOnField: Text value = {}", textValue); - - if (textValue != null) { - String result = textValue.toString(); - // Parse CDS element reference if needed - if (result.startsWith("{==") && result.endsWith("}")) { - result = result.substring(3, result.length() - 1); - } else if (result.startsWith("{") && result.endsWith("}")) { - result = result.substring(1, result.length() - 1); - } - logger.info("getTitleFromCommonTextOnField: Parsed title field = {}", result); - return result; - } - } - } else { - // Also try Common.Text directly (alternate format) - Optional> commonTextOpt = - element.findAnnotation("Common.Text"); - if (commonTextOpt.isPresent()) { - Object textValue = commonTextOpt.get().getValue(); - logger.info("getTitleFromCommonTextOnField: Common.Text value = {}", textValue); - - if (textValue != null) { - String result = textValue.toString(); - // Parse CDS element reference if needed - if (result.startsWith("{==") && result.endsWith("}")) { - result = result.substring(3, result.length() - 1); - } else if (result.startsWith("{") && result.endsWith("}")) { - result = result.substring(1, result.length() - 1); - } - logger.info("getTitleFromCommonTextOnField: Parsed title field = {}", result); - return result; - } - } - } - } + Optional elementOpt = entityOpt.get().findElement(fieldName); + if (!elementOpt.isPresent()) { + return null; + } + + com.sap.cds.reflect.CdsElement element = elementOpt.get(); + logger.info( + "getTitleFromCommonTextOnField: Found element '{}', checking for Common annotation", + fieldName); + + // Try Common annotation first (contains Text property) + String result = extractTextFromCommonAnnotation(element); + if (result != null) { + return result; } + + // Try Common.Text directly as alternate format + return extractTextFromCommonTextAnnotation(element); + } catch (Exception e) { logger.info("getTitleFromCommonTextOnField: Error - {}", e.getMessage(), e); + return null; } - return null; + } + + /** + * Extracts text value from Common annotation's Text property. + * + * @param element the CDS element to check + * @return the parsed text field name, or null if not found + */ + private static String extractTextFromCommonAnnotation(com.sap.cds.reflect.CdsElement element) { + Optional> commonAnnotationOpt = + element.findAnnotation("Common"); + if (!commonAnnotationOpt.isPresent()) { + return null; + } + + Object commonValue = commonAnnotationOpt.get().getValue(); + logger.info( + "getTitleFromCommonTextOnField: Common annotation value type = {}", + commonValue != null ? commonValue.getClass().getSimpleName() : "null"); + + if (!(commonValue instanceof Map)) { + return null; + } + + @SuppressWarnings("unchecked") + Map commonMap = (Map) commonValue; + logger.info("getTitleFromCommonTextOnField: Common map keys = {}", commonMap.keySet()); + + Object textValue = commonMap.get("Text"); + logger.info("getTitleFromCommonTextOnField: Text value = {}", textValue); + + return parseTextValue(textValue, "getTitleFromCommonTextOnField"); + } + + /** + * Extracts text value from Common.Text annotation directly. + * + * @param element the CDS element to check + * @return the parsed text field name, or null if not found + */ + private static String extractTextFromCommonTextAnnotation( + com.sap.cds.reflect.CdsElement element) { + Optional> commonTextOpt = + element.findAnnotation("Common.Text"); + if (!commonTextOpt.isPresent()) { + return null; + } + + Object textValue = commonTextOpt.get().getValue(); + logger.info("getTitleFromCommonTextOnField: Common.Text value = {}", textValue); + + return parseTextValue(textValue, "getTitleFromCommonTextOnField"); + } + + /** + * Parses a text value by removing CDS element reference markers. + * + * @param textValue the raw text value from annotation + * @param logContext context string for logging + * @return the parsed field name, or null if textValue is null + */ + private static String parseTextValue(Object textValue, String logContext) { + if (textValue == null) { + return null; + } + + String result = textValue.toString(); + // Remove CDS annotation wrapper syntax to extract the actual field name + if (result.startsWith("{==") && result.endsWith("}")) { + result = result.substring(3, result.length() - 1); + } else if (result.startsWith("{") && result.endsWith("}")) { + result = result.substring(1, result.length() - 1); + } + logger.info("{}: Parsed title field = {}", logContext, result); + return result; } /** @@ -774,7 +807,8 @@ private static String getSemanticKeyField(CdsModel model, String entityName) { String rawValue = keys.get(0).toString(); logger.info("getSemanticKeyField: Raw value from annotation = {}", rawValue); - // Parse CDS element references like {==ID} or {path} + // Extract field name from CDS annotation format (e.g., curly braces with equals + // prefix) String fieldName = rawValue; if (rawValue.startsWith("{==") && rawValue.endsWith("}")) { fieldName = rawValue.substring(3, rawValue.length() - 1); From 0de9e68b474239775bfbefced522f72f99f0fe1d Mon Sep 17 00:00:00 2001 From: "Yashmeet ." Date: Sat, 14 Feb 2026 14:36:52 +0530 Subject: [PATCH 04/10] review comments --- .../applicationservice/helper/AttachmentsHandlerUtils.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java index 0311a9d4d..08c65fc9b 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java @@ -455,7 +455,7 @@ public static Map getAttachmentParentTitles( String compositionPath = compositionEntry.getValue(); String parentTitle = findParentTitle(model, wrappedEntity, compositionPath, entityName, targetEntity); - if (parentTitle != null) { + if (parentTitle != null && !parentTitle.isEmpty()) { parentTitles.put(compositionPath, parentTitle); } } @@ -526,7 +526,9 @@ private static String findParentTitle( // For nested entities, try to determine the entity type from the composition path String nestedEntityName = determineNestedEntityName(model, targetEntity, entityPart); - return extractTitleFromEntity(model, nestedEntityName, parentList.get(0)); + if (nestedEntityName != null) { + return extractTitleFromEntity(model, nestedEntityName, parentList.get(0)); + } } } } From d7393840f04dea70ea2fd442be78538c4294c96a Mon Sep 17 00:00:00 2001 From: "Yashmeet ." Date: Sat, 14 Feb 2026 14:44:54 +0530 Subject: [PATCH 05/10] sonar fix --- .../helper/AttachmentsHandlerUtils.java | 151 ++++++++++++------ 1 file changed, 103 insertions(+), 48 deletions(-) diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java index 08c65fc9b..958d4dde0 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java @@ -481,65 +481,120 @@ private static String findParentTitle( String compositionPath, String rootEntityName, String targetEntity) { - logger.info( - "findParentTitle: compositionPath={}, rootEntityName={}, targetEntity={}", - compositionPath, - rootEntityName, - targetEntity); - logger.info("findParentTitle: entity keys={}", entity.keySet()); + logFindParentTitleStart(entity, compositionPath, rootEntityName, targetEntity); try { String[] pathParts = compositionPath.split("\\."); logger.info("findParentTitle: pathParts={}", String.join(",", pathParts)); - if (pathParts.length >= 3) { - String entityPart = pathParts[pathParts.length - 2]; // Second to last part (entity name) - logger.info("findParentTitle: entityPart={} (second to last)", entityPart); + if (pathParts.length < 3) { + logger.info("findParentTitle: Returning null - insufficient path parts"); + return null; + } - // Check if this is a direct composition (entity matches root entity) - if (entityPart.equalsIgnoreCase(rootEntityName)) { - // Direct attachment at root level (e.g., "AdminService.Books.references") - logger.info( - "findParentTitle: Direct attachment detected, looking up entity.get({})", - rootEntityName); - Object entityData = entity.get(rootEntityName); - logger.info( - "findParentTitle: entityData type={}, isNull={}", - entityData != null ? entityData.getClass().getSimpleName() : "null", - entityData == null); - return extractTitleFromEntity(model, targetEntity, entityData); - } else { - // Nested attachment (e.g., "AdminService.chapters123.attachments") - logger.info("findParentTitle: Nested attachment detected"); - // Navigate to the parent entity - Object rootEntity = entity.get(rootEntityName); - if (rootEntity instanceof Map) { - @SuppressWarnings("unchecked") - Map rootMap = (Map) rootEntity; - Object parentCollection = rootMap.get(entityPart); - - if (parentCollection instanceof List) { - @SuppressWarnings("unchecked") - List> parentList = (List>) parentCollection; - if (!parentList.isEmpty()) { - // Get title from the first item in the collection - // For nested entities, try to determine the entity type from the composition path - String nestedEntityName = - determineNestedEntityName(model, targetEntity, entityPart); - if (nestedEntityName != null) { - return extractTitleFromEntity(model, nestedEntityName, parentList.get(0)); - } - } - } - } - } + String entityPart = pathParts[pathParts.length - 2]; + logger.info("findParentTitle: entityPart={} (second to last)", entityPart); + + if (entityPart.equalsIgnoreCase(rootEntityName)) { + return handleDirectAttachment(model, entity, rootEntityName, targetEntity); + } else { + return handleNestedAttachment(model, entity, rootEntityName, entityPart, targetEntity); } } catch (Exception e) { logger.warn("Error finding parent title for composition path: " + compositionPath, e); + return null; } + } - logger.info("findParentTitle: Returning null"); - return null; + /** + * Logs the start of findParentTitle operation. + * + * @param entity the entity data structure + * @param compositionPath the composition path + * @param rootEntityName the root entity name + * @param targetEntity the target entity name + */ + private static void logFindParentTitleStart( + Map entity, + String compositionPath, + String rootEntityName, + String targetEntity) { + logger.info( + "findParentTitle: compositionPath={}, rootEntityName={}, targetEntity={}", + compositionPath, + rootEntityName, + targetEntity); + logger.info("findParentTitle: entity keys={}", entity.keySet()); + } + + /** + * Handles direct attachment title extraction. + * + * @param model the CDS model + * @param entity the entity data structure + * @param rootEntityName the root entity name + * @param targetEntity the target entity name + * @return the extracted title, or null if not found + */ + private static String handleDirectAttachment( + CdsModel model, Map entity, String rootEntityName, String targetEntity) { + logger.info( + "findParentTitle: Direct attachment detected, looking up entity.get({})", rootEntityName); + Object entityData = entity.get(rootEntityName); + logger.info( + "findParentTitle: entityData type={}, isNull={}", + entityData != null ? entityData.getClass().getSimpleName() : "null", + entityData == null); + return extractTitleFromEntity(model, targetEntity, entityData); + } + + /** + * Handles nested attachment title extraction. + * + * @param model the CDS model + * @param entity the entity data structure + * @param rootEntityName the root entity name + * @param entityPart the entity part from the path + * @param targetEntity the target entity name + * @return the extracted title, or null if not found + */ + private static String handleNestedAttachment( + CdsModel model, + Map entity, + String rootEntityName, + String entityPart, + String targetEntity) { + logger.info("findParentTitle: Nested attachment detected"); + + Object rootEntity = entity.get(rootEntityName); + if (!(rootEntity instanceof Map)) { + logger.info("findParentTitle: Returning null - rootEntity is not a Map"); + return null; + } + + @SuppressWarnings("unchecked") + Map rootMap = (Map) rootEntity; + Object parentCollection = rootMap.get(entityPart); + + if (!(parentCollection instanceof List)) { + logger.info("findParentTitle: Returning null - parentCollection is not a List"); + return null; + } + + @SuppressWarnings("unchecked") + List> parentList = (List>) parentCollection; + if (parentList.isEmpty()) { + logger.info("findParentTitle: Returning null - parentList is empty"); + return null; + } + + String nestedEntityName = determineNestedEntityName(model, targetEntity, entityPart); + if (nestedEntityName == null) { + logger.info("findParentTitle: Returning null - nestedEntityName is null"); + return null; + } + + return extractTitleFromEntity(model, nestedEntityName, parentList.get(0)); } /** From eb83b5d3a7f2769ba416df852cad71a7fa739800 Mon Sep 17 00:00:00 2001 From: "Yashmeet ." Date: Sat, 14 Feb 2026 14:49:30 +0530 Subject: [PATCH 06/10] Update pom.xml --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 107849b9b..a71a54c5b 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ - 3.0.0 + 1.0.0-RC1 17 ${java.version} ${java.version} From 00a02008d2d0cbb73e13c8e4a73e4ac2c30fc117 Mon Sep 17 00:00:00 2001 From: "Yashmeet ." Date: Mon, 16 Feb 2026 15:38:29 +0530 Subject: [PATCH 07/10] Fix for nested entity --- .../SDMCreateAttachmentsHandler.java | 8 +- .../SDMUpdateAttachmentsHandler.java | 8 +- .../helper/AttachmentsHandlerUtils.java | 369 +++++++++++++++++- 3 files changed, 364 insertions(+), 21 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 84a5c73a1..6f978bee6 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 @@ -142,6 +142,7 @@ public void updateName( String attachmentCompositionDefinition = entry.getKey(); String attachmentCompositionName = entry.getValue().get("name"); String parentTitle = entry.getValue().get("parentTitle"); + String parentTitleMap = entry.getValue().get("parentTitleMap"); Map propertyTitles = new HashMap<>(); Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); String targetEntity = context.getTarget().getQualifiedName(); @@ -160,7 +161,12 @@ public void updateName( context.getModel().findEntity(attachmentCompositionDefinition); isError = AttachmentsHandlerUtils.validateFileNames( - context, data, attachmentCompositionName, contextInfo, attachmentEntity); + context, + data, + attachmentCompositionName, + contextInfo, + attachmentEntity, + parentTitleMap); if (!isError) { List fileNameWithRestrictedCharacters = new ArrayList<>(); List duplicateFileNameList = new ArrayList<>(); 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 05c90c2c4..4b3c8d7ab 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 @@ -137,6 +137,7 @@ public void updateName( String attachmentCompositionDefinition = entry.getKey(); String attachmentCompositionName = entry.getValue().get("name"); String parentTitle = entry.getValue().get("parentTitle"); + String parentTitleMap = entry.getValue().get("parentTitleMap"); Boolean isError = false; // Extract composition name (last part after the final ".") @@ -154,7 +155,12 @@ public void updateName( } isError = AttachmentsHandlerUtils.validateFileNames( - context, data, attachmentCompositionName, contextInfo, attachmentEntity); + context, + data, + attachmentCompositionName, + contextInfo, + attachmentEntity, + parentTitleMap); if (!isError) { renameDocument( attachmentEntity, diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java index 958d4dde0..e6f24c02d 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java @@ -418,6 +418,11 @@ public static Map> getAttachmentCompositionDetails( details.put("definition", definition); details.put("parentTitle", parentTitle); + String parentTitleMap = parentTitles.get(name + ":MAP"); + if (parentTitleMap != null) { + details.put("parentTitleMap", parentTitleMap); + } + attachmentDetails.put(definition, details); } @@ -426,11 +431,13 @@ public static Map> getAttachmentCompositionDetails( /** * Retrieves parent entity titles for each attachment composition found in the entity structure. + * For nested compositions, this returns a map of parent instance IDs to their titles. * *

This method analyzes the entity data structure to identify attachment compositions and * extracts the title (or other identifying field) of the parent entity containing each attachment * composition. It handles both direct attachments at the root level and nested attachments within - * composed entities. + * composed entities. For nested entities with multiple instances (e.g., multiple Chapters), it + * maps each parent instance ID to its title. * * @param model the CDS model containing entity definitions and relationships * @param targetEntity the qualified name of the target entity (e.g., "AdminService.Books") @@ -438,7 +445,9 @@ public static Map> getAttachmentCompositionDetails( * @param compositionPathMapping the mapping of attachment composition paths obtained from * getAttachmentPathMapping * @return a map where keys are attachment composition names and values are the parent entity - * titles, or an empty map if no attachments are found + * titles, or an empty map if no attachments are found. For backward compatibility, returns + * single title for direct attachments, or a special format "parentId:title" for nested + * attachments that can be parsed later. */ public static Map getAttachmentParentTitles( CdsModel model, @@ -453,16 +462,176 @@ public static Map getAttachmentParentTitles( for (Map.Entry compositionEntry : compositionPathMapping.entrySet()) { String compositionPath = compositionEntry.getValue(); - String parentTitle = - findParentTitle(model, wrappedEntity, compositionPath, entityName, targetEntity); - if (parentTitle != null && !parentTitle.isEmpty()) { - parentTitles.put(compositionPath, parentTitle); + Map allParentTitles = + findAllParentTitles(model, wrappedEntity, compositionPath, entityName, targetEntity); + if (allParentTitles != null && !allParentTitles.isEmpty()) { + // For backward compatibility: if single parent, store just the title + // If multiple parents, store in format that can be parsed: + // "parentId1:title1;parentId2:title2" + if (allParentTitles.size() == 1) { + parentTitles.put(compositionPath, allParentTitles.values().iterator().next()); + } else { + // Store all parent titles in a parseable format + StringBuilder sb = new StringBuilder(); + for (Map.Entry parentEntry : allParentTitles.entrySet()) { + if (sb.length() > 0) { + sb.append(";"); + } + sb.append(parentEntry.getKey()).append(":").append(parentEntry.getKey()); + } + parentTitles.put(compositionPath, allParentTitles.values().iterator().next()); + // Also store the map for lookup + parentTitles.put(compositionPath + ":MAP", encodeParentTitleMap(allParentTitles)); + } } } return parentTitles; } + /** + * Encodes a map of parent IDs to titles into a string format for storage. + * + * @param parentTitleMap the map of parent IDs to titles + * @return encoded string in format "parentId1=title1;parentId2=title2" + */ + private static String encodeParentTitleMap(Map parentTitleMap) { + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : parentTitleMap.entrySet()) { + if (sb.length() > 0) { + sb.append(";"); + } + sb.append(entry.getKey()).append("=").append(entry.getValue()); + } + return sb.toString(); + } + + /** + * Decodes a parent title map from encoded string format. + * + * @param encoded the encoded string in format "parentId1=title1;parentId2=title2" + * @return map of parent IDs to titles + */ + private static Map decodeParentTitleMap(String encoded) { + Map result = new HashMap<>(); + if (encoded != null && !encoded.isEmpty()) { + String[] pairs = encoded.split(";"); + for (String pair : pairs) { + String[] parts = pair.split("=", 2); + if (parts.length == 2) { + result.put(parts[0], parts[1]); + } + } + } + return result; + } + + /** + * Gets the parent title for a specific attachment based on its parent reference. For nested + * attachments, uses the attachment's parent ID (up_ field) to find the correct title. + * + * @param attachment the attachment data containing parent reference + * @param compositionName the composition name + * @param parentTitleData the parent title data (may contain encoded map) + * @param compositionPathMapping the composition path mapping + * @return the specific parent title for this attachment + */ + public static String getParentTitleForAttachment( + Map attachment, + String compositionName, + String parentTitleData, + Map compositionPathMapping) { + // Check if we have a map of parent titles (for nested compositions) + String compositionPath = compositionPathMapping.get(compositionName); + if (compositionPath != null) { + String mapKey = compositionPath + ":MAP"; + // Check if encoded map exists in the parent title data structure + // For now, try to extract parent ID from attachment + Object parentId = attachment.get("up_ID"); + if (parentId == null) { + parentId = attachment.get("up__ID"); + } + + if (parentId != null) { + // Try to decode and find specific title + logger.info("getParentTitleForAttachment: Found parent ID {} for attachment", parentId); + // This would require passing the full context through + // For now, we'll implement a workaround in the calling code + } + } + + return parentTitleData; + } + + /** + * Finds all parent titles for a given attachment composition path. For nested compositions with + * multiple parent instances (e.g., multiple Chapters), this returns a map of all parent IDs to + * titles. + * + * @param model the CDS model containing entity definitions and relationships + * @param entity the wrapped entity data structure + * @param compositionPath the composition path (e.g., "AdminService.chapters.attachments" or + * "AdminService.Books.references") + * @param rootEntityName the name of the root entity + * @param targetEntity the qualified name of the target entity + * @return a map where keys are parent instance IDs and values are titles, or null if not found + */ + private static Map findAllParentTitles( + CdsModel model, + Map entity, + String compositionPath, + String rootEntityName, + String targetEntity) { + Map parentTitleMap = new HashMap<>(); + + try { + String[] pathParts = compositionPath.split("\\."); + if (pathParts.length >= 3) { + String entityPart = pathParts[pathParts.length - 2]; // Second to last part (entity name) + + // Check if this is a direct composition (entity matches root entity) + if (entityPart.equalsIgnoreCase(rootEntityName)) { + // Direct attachment at root level (e.g., "AdminService.Books.references") + Object entityData = entity.get(rootEntityName); + String title = extractTitleFromEntity(model, targetEntity, entityData); + if (title != null && !title.trim().isEmpty()) { + parentTitleMap.put("root", title); + } + } else { + // Nested attachment (e.g., "AdminService.chapters.attachments") + Object rootEntity = entity.get(rootEntityName); + if (rootEntity instanceof Map) { + @SuppressWarnings("unchecked") + Map rootMap = (Map) rootEntity; + Object parentCollection = rootMap.get(entityPart); + + if (parentCollection instanceof List) { + @SuppressWarnings("unchecked") + List> parentList = (List>) parentCollection; + String nestedEntityName = determineNestedEntityName(model, targetEntity, entityPart); + + for (Map parentInstance : parentList) { + String title = extractTitleFromEntity(model, nestedEntityName, parentInstance); + if (title != null && !title.trim().isEmpty()) { + Object parentId = parentInstance.get("ID"); + String parentIdStr = + (parentId != null) + ? parentId.toString() + : String.valueOf(parentTitleMap.size()); + parentTitleMap.put(parentIdStr, title); + } + } + } + } + } + } + } catch (Exception e) { + logger.warn("Error finding all parent titles for composition path: " + compositionPath, e); + } + + return parentTitleMap.isEmpty() ? null : parentTitleMap; + } + /** * Finds the parent title for a given attachment composition path. * @@ -568,7 +737,6 @@ private static String handleNestedAttachment( Object rootEntity = entity.get(rootEntityName); if (!(rootEntity instanceof Map)) { - logger.info("findParentTitle: Returning null - rootEntity is not a Map"); return null; } @@ -577,24 +745,37 @@ private static String handleNestedAttachment( Object parentCollection = rootMap.get(entityPart); if (!(parentCollection instanceof List)) { - logger.info("findParentTitle: Returning null - parentCollection is not a List"); return null; } @SuppressWarnings("unchecked") List> parentList = (List>) parentCollection; if (parentList.isEmpty()) { - logger.info("findParentTitle: Returning null - parentList is empty"); return null; } String nestedEntityName = determineNestedEntityName(model, targetEntity, entityPart); if (nestedEntityName == null) { - logger.info("findParentTitle: Returning null - nestedEntityName is null"); return null; } - return extractTitleFromEntity(model, nestedEntityName, parentList.get(0)); + List allTitles = new ArrayList<>(); + for (Map parentInstance : parentList) { + String title = extractTitleFromEntity(model, nestedEntityName, parentInstance); + if (title != null && !title.trim().isEmpty()) { + allTitles.add(title); + } + } + + if (allTitles.isEmpty()) { + return null; + } + + if (allTitles.size() == 1) { + return allTitles.get(0); + } else { + return allTitles.get(0); + } } /** @@ -930,12 +1111,34 @@ private static Object getNestedValue(Map map, String path) { * @param composition the composition name used to locate attachments in the data structure * @return true if any validation errors are found, false otherwise */ + /** + * Validates file names in the provided data for various constraints including whitespace, + * restricted characters, and duplicates. + * + *

This method performs comprehensive validation of file names by checking for: + * + *

    + *
  • Whitespace-only or null file names + *
  • Restricted characters (such as / and \) + *
  • Duplicate file names within the same repository + *
+ * + * @param context the event context containing messages for error reporting + * @param data the list of CDS data containing potential file attachments + * @param composition the composition name used to locate attachments in the data structure + * @param contextInfo the default context info for error messages + * @param attachmentEntity the attachment entity definition + * @param parentTitleMap encoded map of parent IDs to titles (for nested compositions with + * multiple parents) + * @return true if any validation errors are found, false otherwise + */ public static Boolean validateFileNames( EventContext context, List data, String composition, String contextInfo, - Optional attachmentEntity) { + Optional attachmentEntity, + String parentTitleMap) { Boolean isError = false; String targetEntity = context.getTarget().getQualifiedName(); String upIdKey = ""; @@ -943,6 +1146,13 @@ public static Boolean validateFileNames( upIdKey = SDMUtils.getUpIdKey(attachmentEntity.get()); } + // Extract composition name for context + String compositionName = composition; + if (composition != null && composition.contains(".")) { + String[] parts = composition.split("\\."); + compositionName = parts[parts.length - 1]; + } + // Validation for file names Set whitespaceFilenames = SDMUtils.FileNameContainsWhitespace(data, composition, targetEntity); @@ -951,17 +1161,46 @@ public static Boolean validateFileNames( Set duplicateFilenames = SDMUtils.FileNameDuplicateInDrafts(data, composition, targetEntity, upIdKey); - // Collecting all the errors + // For nested compositions, we need to get attachment-specific parent titles + // Collect all attachments first + List> allAttachments = new ArrayList<>(); + for (CdsData entityData : data) { + @SuppressWarnings("unchecked") + Map entityMap = (Map) entityData; + List> attachments = + fetchAttachments(targetEntity, entityMap, composition); + if (attachments != null) { + allAttachments.addAll(attachments); + } + } + + // Collecting all the errors with attachment-specific context if (whitespaceFilenames != null && !whitespaceFilenames.isEmpty()) { - context - .getMessages() - .error(SDMUtils.getErrorMessage("FILENAME_WHITESPACE_ERROR_MESSAGE") + contextInfo); + // Try to find specific context for each file + for (String filename : whitespaceFilenames) { + String specificContext = + findContextForFile( + filename, allAttachments, compositionName, contextInfo, parentTitleMap); + context + .getMessages() + .error(SDMUtils.getErrorMessage("FILENAME_WHITESPACE_ERROR_MESSAGE") + specificContext); + } isError = true; } if (restrictedFileNames != null && !restrictedFileNames.isEmpty()) { - context - .getMessages() - .error(SDMErrorMessages.nameConstraintMessage(restrictedFileNames) + contextInfo); + // Try to find specific context for each file + for (String filename : restrictedFileNames) { + String specificContext = + findContextForFile( + filename, allAttachments, compositionName, contextInfo, parentTitleMap); + context + .getMessages() + .error( + "\"" + + filename + + "\" contains unsupported characters ('/' or '\\'). Rename and try again.\n\n" + + specificContext); + } isError = true; } if (duplicateFilenames != null && !duplicateFilenames.isEmpty()) { @@ -975,6 +1214,56 @@ public static Boolean validateFileNames( return isError; } + /** + * Finds the specific context (parent title) for a given filename by matching it to an attachment. + * + * @param filename the filename to find context for + * @param attachments list of all attachments + * @param compositionName the composition name for context + * @param defaultContext the default context if attachment not found + * @param parentTitleMap encoded map of parent IDs to titles + * @return specific context string for this file + */ + private static String findContextForFile( + String filename, + List> attachments, + String compositionName, + String defaultContext, + String parentTitleMap) { + if (parentTitleMap == null || parentTitleMap.isEmpty()) { + return defaultContext; + } + + for (Map attachment : attachments) { + Object attachmentFilename = attachment.get("fileName"); + if (attachmentFilename != null && attachmentFilename.toString().equals(filename)) { + return getContextInfoForAttachment( + attachment, compositionName, defaultContext.split("Page: ")[1].trim(), parentTitleMap); + } + } + + return defaultContext; + } + + /** + * Validates file names in the provided data (backward compatible method). + * + * @param context the event context containing messages for error reporting + * @param data the list of CDS data containing potential file attachments + * @param composition the composition name used to locate attachments in the data structure + * @param contextInfo the context info for error messages + * @param attachmentEntity the attachment entity definition + * @return true if any validation errors are found, false otherwise + */ + public static Boolean validateFileNames( + EventContext context, + List data, + String composition, + String contextInfo, + Optional attachmentEntity) { + return validateFileNames(context, data, composition, contextInfo, attachmentEntity, null); + } + /** * Fetches attachment data (filename and description) from SDM. * @@ -1193,10 +1482,52 @@ public static CmisDocument prepareCmisDocument( return cmisDocument; } + /** + * Generates context information for error messages, including table and page location. + * + * @param compositionName the composition name (e.g., "attachments") + * @param parentTitle the default parent title + * @return formatted context info string + */ public static String getContextInfo(String compositionName, String parentTitle) { return String.format(SDMErrorMessages.CONTEXT_INFO_TABLE, compositionName) + String.format( SDMErrorMessages.CONTEXT_INFO_PAGE, (parentTitle != null && !parentTitle.trim().isEmpty() ? parentTitle : "Unknown")); } + + /** + * Generates context information for a specific attachment, using its parent reference to find the + * correct parent title for nested compositions. + * + * @param attachment the attachment data (may contain up_ID for nested compositions) + * @param compositionName the composition name (e.g., "attachments") + * @param parentTitle the default parent title + * @param parentTitleMap the encoded map of parent IDs to titles (for nested compositions) + * @return formatted context info string with correct parent title for this attachment + */ + public static String getContextInfoForAttachment( + Map attachment, + String compositionName, + String parentTitle, + String parentTitleMap) { + String specificParentTitle = parentTitle; + + if (parentTitleMap != null && !parentTitleMap.isEmpty()) { + Object parentId = attachment.get("up_ID"); + if (parentId == null) { + parentId = attachment.get("up__ID"); + } + + if (parentId != null) { + Map titleMap = decodeParentTitleMap(parentTitleMap); + String mappedTitle = titleMap.get(parentId.toString()); + if (mappedTitle != null) { + specificParentTitle = mappedTitle; + } + } + } + + return getContextInfo(compositionName, specificParentTitle); + } } From 62e8fad67b6a15e4fc7009ea6d65cc040928341c Mon Sep 17 00:00:00 2001 From: "Yashmeet ." Date: Mon, 16 Feb 2026 15:51:10 +0530 Subject: [PATCH 08/10] sonar fix --- .../helper/AttachmentsHandlerUtils.java | 178 +++++------------- 1 file changed, 44 insertions(+), 134 deletions(-) diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java index e6f24c02d..2bee67c23 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java @@ -586,158 +586,67 @@ private static Map findAllParentTitles( try { String[] pathParts = compositionPath.split("\\."); - if (pathParts.length >= 3) { - String entityPart = pathParts[pathParts.length - 2]; // Second to last part (entity name) - - // Check if this is a direct composition (entity matches root entity) - if (entityPart.equalsIgnoreCase(rootEntityName)) { - // Direct attachment at root level (e.g., "AdminService.Books.references") - Object entityData = entity.get(rootEntityName); - String title = extractTitleFromEntity(model, targetEntity, entityData); - if (title != null && !title.trim().isEmpty()) { - parentTitleMap.put("root", title); - } - } else { - // Nested attachment (e.g., "AdminService.chapters.attachments") - Object rootEntity = entity.get(rootEntityName); - if (rootEntity instanceof Map) { - @SuppressWarnings("unchecked") - Map rootMap = (Map) rootEntity; - Object parentCollection = rootMap.get(entityPart); - - if (parentCollection instanceof List) { - @SuppressWarnings("unchecked") - List> parentList = (List>) parentCollection; - String nestedEntityName = determineNestedEntityName(model, targetEntity, entityPart); - - for (Map parentInstance : parentList) { - String title = extractTitleFromEntity(model, nestedEntityName, parentInstance); - if (title != null && !title.trim().isEmpty()) { - Object parentId = parentInstance.get("ID"); - String parentIdStr = - (parentId != null) - ? parentId.toString() - : String.valueOf(parentTitleMap.size()); - parentTitleMap.put(parentIdStr, title); - } - } - } - } - } - } - } catch (Exception e) { - logger.warn("Error finding all parent titles for composition path: " + compositionPath, e); - } - - return parentTitleMap.isEmpty() ? null : parentTitleMap; - } - - /** - * Finds the parent title for a given attachment composition path. - * - * @param model the CDS model containing entity definitions and relationships - * @param entity the wrapped entity data structure - * @param compositionPath the composition path (e.g., "AdminService.chapters123.attachments" or - * "AdminService.Books.references") - * @param rootEntityName the name of the root entity - * @param targetEntity the qualified name of the target entity - * @return the title of the parent entity containing the attachment composition, or null if not - * found - */ - private static String findParentTitle( - CdsModel model, - Map entity, - String compositionPath, - String rootEntityName, - String targetEntity) { - logFindParentTitleStart(entity, compositionPath, rootEntityName, targetEntity); - - try { - String[] pathParts = compositionPath.split("\\."); - logger.info("findParentTitle: pathParts={}", String.join(",", pathParts)); - if (pathParts.length < 3) { - logger.info("findParentTitle: Returning null - insufficient path parts"); return null; } String entityPart = pathParts[pathParts.length - 2]; - logger.info("findParentTitle: entityPart={} (second to last)", entityPart); if (entityPart.equalsIgnoreCase(rootEntityName)) { - return handleDirectAttachment(model, entity, rootEntityName, targetEntity); + handleDirectAttachmentTitles(model, entity, rootEntityName, targetEntity, parentTitleMap); } else { - return handleNestedAttachment(model, entity, rootEntityName, entityPart, targetEntity); + handleNestedAttachmentTitles( + model, entity, rootEntityName, entityPart, targetEntity, parentTitleMap); } } catch (Exception e) { - logger.warn("Error finding parent title for composition path: " + compositionPath, e); - return null; + logger.warn("Error finding all parent titles for composition path: " + compositionPath, e); } - } - /** - * Logs the start of findParentTitle operation. - * - * @param entity the entity data structure - * @param compositionPath the composition path - * @param rootEntityName the root entity name - * @param targetEntity the target entity name - */ - private static void logFindParentTitleStart( - Map entity, - String compositionPath, - String rootEntityName, - String targetEntity) { - logger.info( - "findParentTitle: compositionPath={}, rootEntityName={}, targetEntity={}", - compositionPath, - rootEntityName, - targetEntity); - logger.info("findParentTitle: entity keys={}", entity.keySet()); + return parentTitleMap.isEmpty() ? null : parentTitleMap; } /** - * Handles direct attachment title extraction. + * Handles title extraction for direct attachments at root level. * * @param model the CDS model * @param entity the entity data structure * @param rootEntityName the root entity name * @param targetEntity the target entity name - * @return the extracted title, or null if not found + * @param parentTitleMap the map to populate with titles */ - private static String handleDirectAttachment( - CdsModel model, Map entity, String rootEntityName, String targetEntity) { - logger.info( - "findParentTitle: Direct attachment detected, looking up entity.get({})", rootEntityName); + private static void handleDirectAttachmentTitles( + CdsModel model, + Map entity, + String rootEntityName, + String targetEntity, + Map parentTitleMap) { Object entityData = entity.get(rootEntityName); - logger.info( - "findParentTitle: entityData type={}, isNull={}", - entityData != null ? entityData.getClass().getSimpleName() : "null", - entityData == null); - return extractTitleFromEntity(model, targetEntity, entityData); + String title = extractTitleFromEntity(model, targetEntity, entityData); + if (title != null && !title.trim().isEmpty()) { + parentTitleMap.put("root", title); + } } /** - * Handles nested attachment title extraction. + * Handles title extraction for nested attachments. * * @param model the CDS model * @param entity the entity data structure * @param rootEntityName the root entity name * @param entityPart the entity part from the path * @param targetEntity the target entity name - * @return the extracted title, or null if not found + * @param parentTitleMap the map to populate with titles */ - private static String handleNestedAttachment( + private static void handleNestedAttachmentTitles( CdsModel model, Map entity, String rootEntityName, String entityPart, - String targetEntity) { - logger.info("findParentTitle: Nested attachment detected"); - + String targetEntity, + Map parentTitleMap) { Object rootEntity = entity.get(rootEntityName); if (!(rootEntity instanceof Map)) { - return null; + return; } @SuppressWarnings("unchecked") @@ -745,37 +654,38 @@ private static String handleNestedAttachment( Object parentCollection = rootMap.get(entityPart); if (!(parentCollection instanceof List)) { - return null; + return; } @SuppressWarnings("unchecked") List> parentList = (List>) parentCollection; - if (parentList.isEmpty()) { - return null; - } - String nestedEntityName = determineNestedEntityName(model, targetEntity, entityPart); - if (nestedEntityName == null) { - return null; - } - List allTitles = new ArrayList<>(); + extractTitlesFromParentList(model, nestedEntityName, parentList, parentTitleMap); + } + + /** + * Extracts titles from a list of parent instances. + * + * @param model the CDS model + * @param nestedEntityName the nested entity name + * @param parentList the list of parent instances + * @param parentTitleMap the map to populate with titles + */ + private static void extractTitlesFromParentList( + CdsModel model, + String nestedEntityName, + List> parentList, + Map parentTitleMap) { for (Map parentInstance : parentList) { String title = extractTitleFromEntity(model, nestedEntityName, parentInstance); if (title != null && !title.trim().isEmpty()) { - allTitles.add(title); + Object parentId = parentInstance.get("ID"); + String parentIdStr = + (parentId != null) ? parentId.toString() : String.valueOf(parentTitleMap.size()); + parentTitleMap.put(parentIdStr, title); } } - - if (allTitles.isEmpty()) { - return null; - } - - if (allTitles.size() == 1) { - return allTitles.get(0); - } else { - return allTitles.get(0); - } } /** From 50dfe8901aa16aa50b0d2040ab5dd5cbbecc6d3f Mon Sep 17 00:00:00 2001 From: "Yashmeet ." Date: Mon, 16 Feb 2026 16:00:19 +0530 Subject: [PATCH 09/10] sonar fix --- .../applicationservice/helper/AttachmentsHandlerUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java index 2bee67c23..0a5ad2f14 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java @@ -587,7 +587,7 @@ private static Map findAllParentTitles( try { String[] pathParts = compositionPath.split("\\."); if (pathParts.length < 3) { - return null; + return parentTitleMap; } String entityPart = pathParts[pathParts.length - 2]; @@ -602,7 +602,7 @@ private static Map findAllParentTitles( logger.warn("Error finding all parent titles for composition path: " + compositionPath, e); } - return parentTitleMap.isEmpty() ? null : parentTitleMap; + return parentTitleMap; } /** From c63927f5d50b20e51f71e530d9d27263f15c724a Mon Sep 17 00:00:00 2001 From: "Yashmeet ." Date: Mon, 16 Feb 2026 16:35:20 +0530 Subject: [PATCH 10/10] multi-tenant workflow changes for testing --- .github/workflows/multiTenancyDeployLocal.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/multiTenancyDeployLocal.yml b/.github/workflows/multiTenancyDeployLocal.yml index 78ae0d4dc..f7ee514e3 100644 --- a/.github/workflows/multiTenancyDeployLocal.yml +++ b/.github/workflows/multiTenancyDeployLocal.yml @@ -59,7 +59,7 @@ jobs: - name: Clone the cloud-cap-samples-java repo 🌐 run: | echo "🔄 Cloning repository..." - git clone --depth 1 --branch local_mtTests https://github.com/vibhutikumar07/cloud-cap-samples-java.git + git clone --depth 1 --branch appChngsForCrctPageTitles https://github.com/vibhutikumar07/cloud-cap-samples-java.git echo "✅ Repository cloned!" - name: Override cds.services.version (runtime only)