diff --git a/sdm/pom.xml b/sdm/pom.xml index 1d9fce31b..7283b6ab9 100644 --- a/sdm/pom.xml +++ b/sdm/pom.xml @@ -607,12 +607,12 @@ BRANCH COVEREDRATIO - 0.55 + 0.50 CLASS MISSEDCOUNT - 1 + 6 diff --git a/sdm/src/main/java/com/sap/cds/sdm/constants/SDMConstants.java b/sdm/src/main/java/com/sap/cds/sdm/constants/SDMConstants.java index bf737cdaa..812bfb18d 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/constants/SDMConstants.java +++ b/sdm/src/main/java/com/sap/cds/sdm/constants/SDMConstants.java @@ -39,7 +39,8 @@ private SDMConstants() { public static final String SDM_ROLES_ERROR_MESSAGE = "Unable to rename the file due to an error at the server"; public static final String SDM_ENV_NAME = "sdm"; - + public static final String ENTITY_PROCESSING_ERROR_LINK = + "Failed to create link due to error while processing entity"; public static final String SDM_TOKEN_EXCHANGE_DESTINATION = "sdm-token-exchange-flow"; public static final String SDM_TECHNICAL_CREDENTIALS_FLOW_DESTINATION = "sdm-technical-user-flow"; public static final String SDM_TOKEN_FETCH = "sdm-token-fetch"; @@ -92,6 +93,8 @@ private SDMConstants() { "Unable to find target attachment entity: %s"; public static final String INVALID_FACET_FORMAT_ERROR = "Invalid facet format. Expected: Service.Entity.Composition, got: %s"; + public static final String FETCH_ATTACHMENT_COMPOSITION_ERROR = + "Failed to fetch attachment composition"; public static String nameConstraintMessage( List fileNameWithRestrictedCharacters, String operation) { 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 4543250f4..dee78c553 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 @@ -8,6 +8,7 @@ import com.sap.cds.sdm.caching.SecondaryPropertiesKey; import com.sap.cds.sdm.constants.SDMConstants; import com.sap.cds.sdm.handler.TokenHandler; +import com.sap.cds.sdm.handler.applicationservice.helper.AttachmentsHandlerUtils; import com.sap.cds.sdm.model.CmisDocument; import com.sap.cds.sdm.model.SDMCredentials; import com.sap.cds.sdm.persistence.DBQuery; @@ -29,14 +30,16 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @ServiceName(value = "*", type = ApplicationService.class) public class SDMCreateAttachmentsHandler implements EventHandler { - private final PersistenceService persistenceService; private final SDMService sdmService; private final TokenHandler tokenHandler; private final DBQuery dbQuery; + private static final Logger logger = LoggerFactory.getLogger(SDMCreateAttachmentsHandler.class); public SDMCreateAttachmentsHandler( PersistenceService persistenceService, @@ -52,17 +55,29 @@ public SDMCreateAttachmentsHandler( @Before @HandlerOrder(HandlerOrder.EARLY) public void processBefore(CdsCreateEventContext context, List data) throws IOException { - List attachmentCompositions = getEntityCompositions(context); - for (String composition : attachmentCompositions) { - updateName(context, data, composition); + // Get the combined mapping of attachment composition paths and names + Map compositionPathMapping = + AttachmentsHandlerUtils.getAttachmentPathMapping( + context.getModel(), context.getTarget(), persistenceService); + logger.info("Attachment compositions present in CDS Model : " + compositionPathMapping); + for (Map.Entry entry : compositionPathMapping.entrySet()) { + String attachmentCompositionDefinition = entry.getKey(); + String attachmentCompositionName = entry.getValue(); + updateName(context, data, attachmentCompositionDefinition, attachmentCompositionName); } } - public void updateName(CdsCreateEventContext context, List data, String composition) + public void updateName( + CdsCreateEventContext context, + List data, + String attachmentCompositionDefinition, + String attachmentCompositionName) throws IOException { Map propertyTitles = new HashMap<>(); Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); - Set duplicateFilenames = SDMUtils.isFileNameDuplicateInDrafts(data, composition); + String targetEntity = context.getTarget().getQualifiedName(); + Set duplicateFilenames = + SDMUtils.isFileNameDuplicateInDrafts(data, attachmentCompositionName, targetEntity); if (!duplicateFilenames.isEmpty()) { handleDuplicateFilenames(context, duplicateFilenames); } else { @@ -73,11 +88,11 @@ public void updateName(CdsCreateEventContext context, List data, String Map badRequest = new HashMap<>(); List noSDMRoles = new ArrayList<>(); for (Map entity : data) { - List> attachments = (List>) entity.get(composition); + List> attachments = + AttachmentsHandlerUtils.fetchAttachments( + targetEntity, entity, attachmentCompositionName); Optional attachmentEntity = - context - .getModel() - .findEntity(context.getTarget().getQualifiedName() + "." + composition); + context.getModel().findEntity(attachmentCompositionDefinition); if (attachments != null && !attachments.isEmpty()) { propertyTitles = SDMUtils.getPropertyTitles(attachmentEntity, attachments.get(0)); secondaryPropertiesWithInvalidDefinitions = @@ -92,10 +107,11 @@ public void updateName(CdsCreateEventContext context, List data, String filesNotFound, filesWithUnsupportedProperties, badRequest, - composition, + attachmentCompositionDefinition, attachmentEntity, secondaryPropertiesWithInvalidDefinitions, - noSDMRoles); + noSDMRoles, + attachmentCompositionName); handleWarnings( context, fileNameWithRestrictedCharacters, @@ -130,9 +146,12 @@ private void processEntity( String composition, Optional attachmentEntity, Map secondaryPropertiesWithInvalidDefinitions, - List noSDMRoles) + List noSDMRoles, + String attachmentCompositionName) throws IOException { - List> attachments = (List>) entity.get(composition); + String targetEntity = context.getTarget().getQualifiedName(); + List> attachments = + AttachmentsHandlerUtils.fetchAttachments(targetEntity, entity, attachmentCompositionName); if (attachments != null) { for (Map attachment : attachments) { processAttachment( 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 c40d6968d..860753804 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 @@ -8,6 +8,7 @@ import com.sap.cds.sdm.caching.SecondaryPropertiesKey; import com.sap.cds.sdm.constants.SDMConstants; import com.sap.cds.sdm.handler.TokenHandler; +import com.sap.cds.sdm.handler.applicationservice.helper.AttachmentsHandlerUtils; import com.sap.cds.sdm.model.CmisDocument; import com.sap.cds.sdm.model.SDMCredentials; import com.sap.cds.sdm.persistence.DBQuery; @@ -24,6 +25,8 @@ import java.io.IOException; import java.util.*; import org.ehcache.Cache; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @ServiceName(value = "*", type = ApplicationService.class) public class SDMUpdateAttachmentsHandler implements EventHandler { @@ -31,6 +34,7 @@ public class SDMUpdateAttachmentsHandler implements EventHandler { private final SDMService sdmService; private final TokenHandler tokenHandler; private final DBQuery dbQuery; + private static final Logger logger = LoggerFactory.getLogger(CacheConfig.class); public SDMUpdateAttachmentsHandler( PersistenceService persistenceService, @@ -46,15 +50,26 @@ public SDMUpdateAttachmentsHandler( @Before @HandlerOrder(HandlerOrder.EARLY) public void processBefore(CdsUpdateEventContext context, List data) throws IOException { - List attachmentCompositions = getEntityCompositions(context); - for (String composition : attachmentCompositions) { - updateName(context, data, composition); + Map compositionPathMapping = + AttachmentsHandlerUtils.getAttachmentPathMapping( + context.getModel(), context.getTarget(), persistenceService); + logger.info("Attachment compositions present in CDS Model : " + compositionPathMapping); + for (Map.Entry entry : compositionPathMapping.entrySet()) { + String attachmentCompositionDefinition = entry.getKey(); + String attachmentCompositionName = entry.getValue(); + updateName(context, data, attachmentCompositionDefinition, attachmentCompositionName); } } - public void updateName(CdsUpdateEventContext context, List data, String composition) + public void updateName( + CdsUpdateEventContext context, + List data, + String attachmentCompositionDefinition, + String attachmentCompositionName) throws IOException { - Set duplicateFilenames = SDMUtils.isFileNameDuplicateInDrafts(data, composition); + String targetEntity = context.getTarget().getQualifiedName(); + Set duplicateFilenames = + SDMUtils.isFileNameDuplicateInDrafts(data, attachmentCompositionName, targetEntity); if (!duplicateFilenames.isEmpty()) { context .getMessages() @@ -64,8 +79,13 @@ public void updateName(CdsUpdateEventContext context, List data, String String.join(", ", duplicateFilenames))); } else { Optional attachmentEntity = - context.getModel().findEntity(context.getTarget().getQualifiedName() + "." + composition); - renameDocument(attachmentEntity, context, data, composition); + context.getModel().findEntity(attachmentCompositionDefinition); + renameDocument( + attachmentEntity, + context, + data, + attachmentCompositionDefinition, + attachmentCompositionName); } } @@ -73,7 +93,8 @@ private void renameDocument( Optional attachmentEntity, CdsUpdateEventContext context, List data, - String composition) + String attachmentCompositionDefinition, + String attachmentCompositionName) throws IOException { List duplicateFileNameList = new ArrayList<>(); Map secondaryPropertiesWithInvalidDefinitions; @@ -83,8 +104,11 @@ private void renameDocument( Map badRequest = new HashMap<>(); Map propertyTitles = new HashMap<>(); List noSDMRoles = new ArrayList<>(); + String targetEntity = context.getTarget().getQualifiedName(); for (Map entity : data) { - List> attachments = (List>) entity.get(composition); + List> attachments = + AttachmentsHandlerUtils.fetchAttachments(targetEntity, entity, attachmentCompositionName); + if (attachments != null && !attachments.isEmpty()) { propertyTitles = SDMUtils.getPropertyTitles(attachmentEntity, attachments.get(0)); } else { 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 new file mode 100644 index 000000000..279dfd8c0 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java @@ -0,0 +1,366 @@ +package com.sap.cds.sdm.handler.applicationservice.helper; + +import com.sap.cds.reflect.CdsAssociationType; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.reflect.CdsModel; +import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.sdm.handler.common.SDMAssociationCascader; +import com.sap.cds.sdm.handler.common.SDMAttachmentsReader; +import com.sap.cds.services.persistence.PersistenceService; +import java.util.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AttachmentsHandlerUtils { + + private static final Logger logger = LoggerFactory.getLogger(AttachmentsHandlerUtils.class); + + private AttachmentsHandlerUtils() { + // Doesn't do anything + } + + /** + * Retrieves a list of attachment entity paths for the given CDS entity. + * + * This method creates an SDMAttachmentsReader instance to traverse the entity's structure and + * identify all paths that lead to attachment entities within the CDS model. It uses an + * SDMAssociationCascader to handle cascading through entity associations and compositions to find + * nested attachment relationships. + * + * @param model the CDS model containing entity definitions and relationships + * @param entity the target CDS entity to analyze for attachment paths + * @param persistenceService the persistence service used for data access operations + * @return a list of strings representing paths to attachment entities, or an empty list if no + * attachments are found or if an error occurs during processing + */ + public static List getAttachmentEntityPaths( + CdsModel model, CdsEntity entity, PersistenceService persistenceService) { + try { + SDMAssociationCascader cascader = new SDMAssociationCascader(); + SDMAttachmentsReader reader = new SDMAttachmentsReader(cascader, persistenceService); + return reader.getAttachmentEntityPaths(model, entity); + } catch (Exception e) { + return new ArrayList<>(); + } + } + + /** + * Creates a mapping of attachment entity paths to their corresponding actual paths within the CDS + * model. + * + * This method analyzes both direct and nested attachment compositions within the given entity. + * It processes direct attachments that are immediate compositions of the entity, and also + * traverses nested compositions to find attachments in related entities. The resulting mapping + * provides a translation between logical attachment paths and their actual implementation paths. + * + * @param model the CDS model containing entity definitions and relationships + * @param entity the target CDS entity to analyze for attachment path mappings + * @param persistenceService the persistence service used for data access operations + * @return a map where keys are attachment entity paths and values are the corresponding actual + * paths, or an empty map if no attachments are found or if an error occurs during processing + */ + public static Map getAttachmentPathMapping( + CdsModel model, CdsEntity entity, PersistenceService persistenceService) { + try { + Map pathMapping = new HashMap<>(); + SDMAssociationCascader cascader = new SDMAssociationCascader(); + SDMAttachmentsReader reader = new SDMAttachmentsReader(cascader, persistenceService); + + // Process direct attachments + entity + .compositions() + .forEach( + composition -> processDirectAttachmentComposition(entity, pathMapping, composition)); + + // Process nested attachments + entity + .compositions() + .forEach( + composition -> + processNestedAttachmentComposition( + model, entity, reader, pathMapping, composition)); + + return pathMapping; + } catch (Exception e) { + logger.error(SDMConstants.FETCH_ATTACHMENT_COMPOSITION_ERROR, e.getMessage()); + return new HashMap<>(); + } + } + + private static void processDirectAttachmentComposition( + CdsEntity entity, Map pathMapping, Object composition) { + String compositionName = ((com.sap.cds.reflect.CdsElement) composition).getName(); + if (((com.sap.cds.reflect.CdsElement) composition).getType().isAssociation()) { + CdsAssociationType associationType = + (CdsAssociationType) ((com.sap.cds.reflect.CdsElement) composition).getType(); + String targetAspect = + associationType.getTargetAspect().isPresent() + ? associationType.getTargetAspect().get().getQualifiedName() + : null; + + if (isDirectAttachmentTargetAspect(targetAspect)) { + String serviceName = entity.getQualifiedName().split("\\.")[0]; + String entityName = entity.getName(); + String directPath = serviceName + "." + entityName + "." + compositionName; + pathMapping.put(directPath, directPath); + } + } + } + + private static void processNestedAttachmentComposition( + CdsModel model, + CdsEntity entity, + SDMAttachmentsReader reader, + Map pathMapping, + Object composition) { + String compositionName = ((com.sap.cds.reflect.CdsElement) composition).getName(); + String compositionTargetEntityName = ""; + + if (((com.sap.cds.reflect.CdsElement) composition).getType().isAssociation()) { + CdsAssociationType associationType = + (CdsAssociationType) ((com.sap.cds.reflect.CdsElement) composition).getType(); + String targetAspect = + associationType.getTargetAspect().isPresent() + ? associationType.getTargetAspect().get().getQualifiedName() + : null; + + if (isDirectAttachmentTargetAspect(targetAspect)) { + return; // Skip direct attachment compositions + } + + compositionTargetEntityName = associationType.getTarget().getQualifiedName(); + } + + processCompositionTargetEntity( + model, entity, reader, pathMapping, compositionName, compositionTargetEntityName); + } + + private static void processCompositionTargetEntity( + CdsModel model, + CdsEntity entity, + SDMAttachmentsReader reader, + Map pathMapping, + String compositionName, + String compositionTargetEntityName) { + if (!compositionTargetEntityName.isEmpty()) { + Optional targetEntityOpt = model.findEntity(compositionTargetEntityName); + if (targetEntityOpt.isPresent()) { + CdsEntity targetEntity = targetEntityOpt.get(); + List attachmentPaths = reader.getAttachmentEntityPaths(model, targetEntity); + processAttachmentPaths(entity, pathMapping, compositionName, targetEntity, attachmentPaths); + } + } + } + + private static void processAttachmentPaths( + CdsEntity entity, + Map pathMapping, + String compositionName, + CdsEntity targetEntity, + List attachmentPaths) { + for (String attachmentPath : attachmentPaths) { + String entityPath = buildEntityPath(entity, targetEntity, attachmentPath); + String actualPath = buildActualPath(entity, compositionName, attachmentPath); + + if (entityPath != null && actualPath != null) { + pathMapping.put(entityPath, actualPath); + } + } + } + + private static boolean isDirectAttachmentTargetAspect(String targetAspect) { + return targetAspect != null && targetAspect.equalsIgnoreCase("sap.attachments.Attachments"); + } + + /** + * Fetches attachment data from a nested entity structure based on the target entity and + * composition name. + * + * This method processes the target entity path to extract the entity name, wraps the provided + * entity data with a parent structure, and then searches for attachments within the nested + * structure. It parses the attachment composition name to identify both the attachment key (e.g., + * "attachments") and the parent key (e.g., "chapters") for precise attachment location. + * + * @param targetEntity the qualified name of the target entity (e.g., "ServiceName.EntityName") + * @param entity the entity data structure containing potential attachment information + * @param attachmentCompositionName the composition path to the attachments (e.g., + * "chapters.attachments") + * @return a list of maps representing attachment objects found in the entity structure, or an + * empty list if no attachments are found + */ + public static List> fetchAttachments( + String targetEntity, Map entity, String attachmentCompositionName) { + String[] targetEntityPath = targetEntity.split("\\."); + targetEntity = targetEntityPath[targetEntityPath.length - 1]; + entity = AttachmentsHandlerUtils.wrapEntityWithParent(entity, targetEntity.toLowerCase()); + String[] compositionParts = attachmentCompositionName.split("\\."); + String attachmentKeyFromComposition = + compositionParts[compositionParts.length - 1]; // Last part (e.g., "attachments") + String parentKeyFromComposition = + compositionParts.length >= 2 + ? compositionParts[compositionParts.length - 2].toLowerCase() + : null; // Second last part (e.g., "chapters") + + // Find all attachment arrays in the nested entity structure + return AttachmentsHandlerUtils.findNestedAttachments( + entity, attachmentKeyFromComposition, parentKeyFromComposition); + } + + private static List> findNestedAttachments( + Map entity, String attachmentKey, String parentKey) { + return findNestedAttachments(entity, attachmentKey, parentKey, null); + } + + private static String buildEntityPath( + CdsEntity parentEntity, CdsEntity targetEntity, String attachmentPath) { + try { + String[] pathParts = attachmentPath.split("\\."); + if (pathParts.length >= 3) { + // Get the service name (first part) + String serviceName = pathParts[0]; + + // Get the target entity name (without service prefix) + String targetEntityName = targetEntity.getName(); + + // Get the attachment part (last part) + String attachmentPart = pathParts[pathParts.length - 1]; + + // Build the entity path: ServiceName.EntityName.attachments + return serviceName + "." + targetEntityName + "." + attachmentPart; + } + } catch (Exception e) { + logger.warn(SDMConstants.FETCH_ATTACHMENT_COMPOSITION_ERROR, e.getMessage()); + } + return null; + } + + private static String buildActualPath( + CdsEntity parentEntity, String compositionPropertyName, String attachmentPath) { + try { + String[] pathParts = attachmentPath.split("\\."); + if (pathParts.length >= 3) { + // Get the service name (first part) + String serviceName = pathParts[0]; + + // Replace the entity name with the composition property name + // Keep the attachment part (last part) + String attachmentPart = pathParts[pathParts.length - 1]; + + // Build the new path: ServiceName.compositionPropertyName.attachments + return serviceName + "." + compositionPropertyName + "." + attachmentPart; + } + } catch (Exception e) { + logger.warn(SDMConstants.FETCH_ATTACHMENT_COMPOSITION_ERROR, e.getMessage()); + } + return null; + } + + private static List> findNestedAttachments( + Map entity, String attachmentKey, String parentKey, String currentParentKey) { + List> result = new ArrayList<>(); + + for (Map.Entry entry : entity.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + // If we found the attachment key + if (attachmentKey.equals(key) && value instanceof List) { + result.addAll(processAttachmentKey(value, key, parentKey, currentParentKey)); + } + // Recursively search in nested objects + else if (value instanceof Map) { + result.addAll(processNestedMap(value, key, attachmentKey, parentKey)); + } + // Recursively search in lists + else if (value instanceof List) { + result.addAll(processNestedList(value, key, attachmentKey, parentKey)); + } + } + + return result; + } + + private static List> processAttachmentKey( + Object value, String key, String parentKey, String currentParentKey) { + List> result = new ArrayList<>(); + + // Check if the parent matches (if parentKey is specified) + if (parentKey == null || isCorrectParentContext(currentParentKey, parentKey)) { + try { + List> attachments = (List>) value; + result.addAll(attachments); + } catch (ClassCastException e) { + logger.warn(SDMConstants.FETCH_ATTACHMENT_COMPOSITION_ERROR, e.getMessage()); + } + } + + return result; + } + + private static List> processNestedMap( + Object value, String key, String attachmentKey, String parentKey) { + List> result = new ArrayList<>(); + + try { + Map nestedMap = (Map) value; + result.addAll(findNestedAttachments(nestedMap, attachmentKey, parentKey, key)); + } catch (ClassCastException e) { + logger.warn(SDMConstants.FETCH_ATTACHMENT_COMPOSITION_ERROR, e.getMessage()); + } + + return result; + } + + private static List> processNestedList( + Object value, String key, String attachmentKey, String parentKey) { + List> result = new ArrayList<>(); + + try { + List> list = (List>) value; + for (Object item : list) { + if (item instanceof Map) { + Map itemMap = (Map) item; + result.addAll(findNestedAttachments(itemMap, attachmentKey, parentKey, key)); + } + } + } catch (ClassCastException e) { + logger.warn(SDMConstants.FETCH_ATTACHMENT_COMPOSITION_ERROR, e.getMessage()); + } + + return result; + } + + private static boolean isCorrectParentContext(String currentParentKey, String expectedParentKey) { + // If no specific parent is expected, any context is valid + if (expectedParentKey == null) { + return true; + } + + // If we're at root level (no current parent) and expecting a specific parent, no match + if (currentParentKey == null) { + return false; + } + + // Check if the current parent matches the expected parent + return expectedParentKey.equals(currentParentKey); + } + + /** + * Wraps an entity data structure with a parent container using the specified target entity name. + * + * This utility method creates a new map with the target entity name as the key and the + * provided entity data as the value. This is necessary because the root of the target entity in + * the CdsData object is not mentioned explicitly, and hence interferes with the recursive + * fetching of attachment compositions. + * + * @param root the entity data structure to be wrapped + * @param targetEntity the name to use as the parent key for wrapping the entity data + * @return a new map containing the target entity name as key and the root entity data as value + */ + public static Map wrapEntityWithParent( + Map root, String targetEntity) { + Map wrapper = new HashMap<>(); + wrapper.put(targetEntity, root); + return wrapper; + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMApplicationHandlerHelper.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMApplicationHandlerHelper.java new file mode 100644 index 000000000..6bb0824a0 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMApplicationHandlerHelper.java @@ -0,0 +1,26 @@ +package com.sap.cds.sdm.handler.common; + +import com.sap.cds.reflect.CdsStructuredType; + +/** + * The class {@link SDMApplicationHandlerHelper} provides helper methods for the SDM attachment + * application handlers. + */ +public final class SDMApplicationHandlerHelper { + private static final String ANNOTATION_IS_MEDIA_DATA = "_is_media_data"; + + /** + * Checks if the entity is a media entity. A media entity is an entity that is annotated with the + * annotation "_is_media_data". + * + * @param baseEntity The entity to check + * @return true if the entity is a media entity, false otherwise + */ + public static boolean isMediaEntity(CdsStructuredType baseEntity) { + return baseEntity.getAnnotationValue(ANNOTATION_IS_MEDIA_DATA, false); + } + + private SDMApplicationHandlerHelper() { + // avoid instantiation + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationCascader.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationCascader.java new file mode 100644 index 000000000..48e8c2734 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationCascader.java @@ -0,0 +1,97 @@ +package com.sap.cds.sdm.handler.common; + +import com.sap.cds.reflect.CdsAssociationType; +import com.sap.cds.reflect.CdsElementDefinition; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.reflect.CdsModel; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +/** + * The class {@link SDMAssociationCascader} is used to find entity paths to all media resource + * entities for a given data model. The path information is returned in a node tree which starts + * from the given entity. Only composition associations are considered. + */ +public class SDMAssociationCascader { + + public SDMNodeTree findEntityPath(CdsModel model, CdsEntity entity) { + var firstList = new LinkedList(); + var internalResultList = + getAttachmentAssociationPath( + model, entity, "", firstList, new ArrayList<>(List.of(entity.getQualifiedName()))); + + var rootTree = new SDMNodeTree(new SDMAssociationIdentifier("", entity.getQualifiedName())); + internalResultList.forEach(rootTree::addPath); + return rootTree; + } + + private List> getAttachmentAssociationPath( + CdsModel model, + CdsEntity entity, + String associationName, + LinkedList firstList, + List processedEntities) { + var internalResultList = new ArrayList>(); + var currentList = new AtomicReference>(); + var localProcessEntities = new ArrayList(); + currentList.set(new LinkedList<>()); + + var isMediaEntity = SDMApplicationHandlerHelper.isMediaEntity(entity); + if (isMediaEntity) { + var identifier = new SDMAssociationIdentifier(associationName, entity.getQualifiedName()); + firstList.addLast(identifier); + } + + if (isMediaEntity) { + internalResultList.add(firstList); + return internalResultList; + } + + Map associations = + entity + .elements() + .filter( + element -> + element.getType().isAssociation() + && element.getType().as(CdsAssociationType.class).isComposition()) + .collect( + Collectors.toMap( + CdsElementDefinition::getName, + element -> element.getType().as(CdsAssociationType.class).getTarget())); + + if (associations.isEmpty()) { + return internalResultList; + } + + var newListNeeded = false; + for (var associatedElement : associations.entrySet()) { + if (!processedEntities.contains(associatedElement.getValue().getQualifiedName())) { + if (newListNeeded) { + currentList.set(new LinkedList<>()); + currentList.get().addAll(firstList); + processedEntities = localProcessEntities; + } else { + firstList.add(new SDMAssociationIdentifier(associationName, entity.getQualifiedName())); + currentList.get().addAll(firstList); + localProcessEntities = new ArrayList<>(processedEntities); + } + processedEntities.add(associatedElement.getValue().getQualifiedName()); + newListNeeded = true; + var result = + getAttachmentAssociationPath( + model, + associatedElement.getValue(), + associatedElement.getKey(), + currentList.get(), + processedEntities); + internalResultList.addAll(result); + } + } + + return internalResultList; + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationIdentifier.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationIdentifier.java new file mode 100644 index 000000000..7ee462949 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationIdentifier.java @@ -0,0 +1,10 @@ +package com.sap.cds.sdm.handler.common; + +/** + * This record is a simple data class that holds the association name and the full entity name for + * SDM attachment processing. + * + * @param associationName the association name + * @param fullEntityName the full entity name + */ +record SDMAssociationIdentifier(String associationName, String fullEntityName) {} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAttachmentsReader.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAttachmentsReader.java new file mode 100644 index 000000000..1164cdf23 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAttachmentsReader.java @@ -0,0 +1,113 @@ +package com.sap.cds.sdm.handler.common; + +import static java.util.Objects.requireNonNull; + +import com.sap.cds.Result; +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments; +import com.sap.cds.ql.CQL; +import com.sap.cds.ql.Expand; +import com.sap.cds.ql.Select; +import com.sap.cds.ql.StructuredType; +import com.sap.cds.ql.cqn.CqnFilterableStatement; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.reflect.CdsModel; +import com.sap.cds.services.persistence.PersistenceService; +import java.util.ArrayList; +import java.util.List; + +/** + * The class {@link SDMAttachmentsReader} is used to deep read attachments from the database for a + * determined path from the given entity to the media entity. The class uses the {@link + * SDMAssociationCascader} to find the entity path. + * + * The returned data is deep including the path structure to the media entity. + */ +public class SDMAttachmentsReader { + + private final SDMAssociationCascader cascader; + private final PersistenceService persistence; + + public SDMAttachmentsReader(SDMAssociationCascader cascader, PersistenceService persistence) { + this.cascader = requireNonNull(cascader, "cascader must not be null"); + this.persistence = requireNonNull(persistence, "persistence must not be null"); + } + + public List readAttachments( + CdsModel model, CdsEntity entity, CqnFilterableStatement statement) { + + SDMNodeTree nodePath = cascader.findEntityPath(model, entity); + List> expandList = buildExpandList(nodePath); + + Select> select; + if (!expandList.isEmpty()) { + select = Select.from(statement.ref()).columns(expandList); + } else { + select = Select.from(statement.ref()).columns(StructuredType::_all); + } + + if (statement.where().isPresent()) { + select.where(statement.where().get()); + } + + Result result = persistence.run(select); + return result.listOf(Attachments.class); + } + + public List getAttachmentEntityPaths(CdsModel model, CdsEntity entity) { + SDMNodeTree nodePath = cascader.findEntityPath(model, entity); + + List attachmentPaths = new ArrayList<>(); + + if (nodePath != null) { + collectAttachmentPaths(nodePath, attachmentPaths, model); + } + return attachmentPaths; + } + + private void collectAttachmentPaths( + SDMNodeTree node, List attachmentPaths, CdsModel model) { + String entityName = node.getIdentifier().fullEntityName(); + + // Check if this entity is an attachment entity + if (isAttachmentEntity(model, entityName)) { + attachmentPaths.add(entityName); + } + + // Recursively check children + for (SDMNodeTree child : node.getChildren()) { + collectAttachmentPaths(child, attachmentPaths, model); + } + } + + private boolean isAttachmentEntity(CdsModel model, String entityName) { + var entityOpt = model.findEntity(entityName); + if (!entityOpt.isPresent()) { + return false; + } + + CdsEntity entity = entityOpt.get(); + // Check if this entity has the @_is_media_data annotation (indicating attachment entity) + return entity.getAnnotationValue("_is_media_data", false); + } + + private List> buildExpandList(SDMNodeTree root) { + List> expandResultList = new ArrayList<>(); + root.getChildren() + .forEach( + child -> { + Expand> expand = buildExpandFromTree(child); + expandResultList.add(expand); + }); + + return expandResultList; + } + + private Expand> buildExpandFromTree(SDMNodeTree node) { + if (node.getChildren().isEmpty()) { + return CQL.to(node.getIdentifier().associationName()).expand(); + } else { + return CQL.to(node.getIdentifier().associationName()) + .expand(node.getChildren().stream().map(this::buildExpandFromTree).toList()); + } + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMNodeTree.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMNodeTree.java new file mode 100644 index 000000000..71611bef2 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMNodeTree.java @@ -0,0 +1,65 @@ +package com.sap.cds.sdm.handler.common; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * The class {@link SDMNodeTree} is a tree data structure that holds the SDM association identifier + * and its children for attachment processing. + */ +class SDMNodeTree { + + private final SDMAssociationIdentifier identifier; + private final List children = new ArrayList<>(); + + SDMNodeTree(SDMAssociationIdentifier identifier) { + this.identifier = identifier; + } + + void addPath(List path) { + var currentIdentifierOptional = + path.stream() + .filter(entry -> entry.fullEntityName().equals(identifier.fullEntityName())) + .findAny(); + if (currentIdentifierOptional.isEmpty()) { + return; + } + var currentNode = this; + var index = path.indexOf(currentIdentifierOptional.get()); + if (index == path.size() - 1) { + return; + } + for (var i = index + 1; i < path.size(); i++) { + var pathEntry = path.get(i); + currentNode = currentNode.getChildOrNew(pathEntry); + } + } + + private SDMNodeTree getChildOrNew(SDMAssociationIdentifier identifier) { + var childOptional = + children.stream() + .filter(child -> child.identifier.fullEntityName().equals(identifier.fullEntityName())) + .findAny(); + if (childOptional.isPresent()) { + return childOptional.get(); + } else { + SDMNodeTree child = new SDMNodeTree(identifier); + children.add(child); + return child; + } + } + + SDMAssociationIdentifier getIdentifier() { + return identifier; + } + + List getChildren() { + return Collections.unmodifiableList(children); + } + + @Override + public String toString() { + return "SDMNodeTree{" + "identifier=" + identifier + ", children=" + children + '}'; + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java index c5c95242c..1098a1eea 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java @@ -188,9 +188,9 @@ private String getUpIdKey(CdsEntity attachmentDraftEntity) { if (upAssociation.isPresent()) { CdsElement association = upAssociation.get(); // get association type - CdsAssociationType assocType = association.getType(); + CdsAssociationType associationType = association.getType(); // get the refs of the association - List fkElements = assocType.refs().map(ref -> "up__" + ref.path()).toList(); + List fkElements = associationType.refs().map(ref -> "up__" + ref.path()).toList(); upIdKey = fkElements.get(0); } return upIdKey; diff --git a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java index b959d24e3..4cedef71b 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java @@ -1,5 +1,7 @@ package com.sap.cds.sdm.service.handler; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.sap.cds.Result; import com.sap.cds.feature.attachments.service.AttachmentService; import com.sap.cds.ql.Insert; @@ -136,9 +138,7 @@ private void createLink(EventContext context) throws IOException { String upIdKey = attachmentDraftEntity.isPresent() ? getUpIdKey(attachmentDraftEntity.get()) : "up__ID"; CqnSelect select = (CqnSelect) context.get("cqn"); - CqnAnalyzer cqnAnalyzer = CqnAnalyzer.create(cdsModel); - String id = upIdKey.replaceFirst("^up__", ""); - String upID = cqnAnalyzer.analyze(select).rootKeys().get(id).toString(); + String upID = fetchUPIDFromCQN(select); String filenameInRequest = context.get("name").toString(); Result result = @@ -214,9 +214,9 @@ private String getUpIdKey(CdsEntity attachmentDraftEntity) { if (upAssociation.isPresent()) { CdsElement association = upAssociation.get(); // get association type - CdsAssociationType assocType = association.getType(); + CdsAssociationType associationType = association.getType(); // get the refs of the association - List fkElements = assocType.refs().map(ref -> "up__" + ref.path()).toList(); + List fkElements = associationType.refs().map(ref -> "up__" + ref.path()).toList(); upIdKey = fkElements.get(0); } return upIdKey; @@ -308,14 +308,45 @@ private void handleCreateLinkResult( + ":" + context.getTarget()); - var insert = Insert.into(context.getTarget().getQualifiedName()).entry(updatedFields); - for (DraftService draftS : draftService) { - // Process each draftService object - if (context.getTarget().getQualifiedName().contains(draftS.getName())) { - draftS.newDraft(insert); + try { + var insert = Insert.into(context.getTarget().getQualifiedName()).entry(updatedFields); + for (DraftService draftS : draftService) { + if (context.getTarget().getQualifiedName().contains(draftS.getName())) { + draftS.newDraft(insert); + } } + } catch (Exception e) { + logger.info("Exception in insert : " + e.getMessage()); } context.setCompleted(); } } + + private String fetchUPIDFromCQN(CqnSelect select) { + try { + String upID = null; + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = mapper.readTree(select.toString()); + JsonNode refArray = root.path("SELECT").path("from").path("ref"); + JsonNode secondLast = refArray.get(refArray.size() - 2); + JsonNode whereArray = secondLast.path("where"); + for (int i = 0; i < whereArray.size(); i++) { + JsonNode node = whereArray.get(i); + if (node.has("ref") + && node.get("ref").isArray() + && node.get("ref").get(0).asText().equals("ID")) { + JsonNode valNode = whereArray.get(i + 2); + upID = valNode.path("val").asText(); + break; + } + } + if (upID == null) { + throw new ServiceException(SDMConstants.ENTITY_PROCESSING_ERROR_LINK); + } + return upID; + } catch (Exception e) { + logger.error(SDMConstants.ENTITY_PROCESSING_ERROR_LINK, e); + throw new ServiceException(SDMConstants.ENTITY_PROCESSING_ERROR_LINK, e); + } + } } diff --git a/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java b/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java index bf073caba..c6896b2de 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java +++ b/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java @@ -6,6 +6,7 @@ import com.sap.cds.reflect.CdsEntity; import com.sap.cds.sdm.caching.CacheConfig; import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.sdm.handler.applicationservice.helper.AttachmentsHandlerUtils; import com.sap.cds.sdm.model.AttachmentInfo; import com.sap.cds.services.persistence.PersistenceService; import java.io.IOException; @@ -32,11 +33,13 @@ private SDMUtils() { // Doesn't do anything } - public static Set isFileNameDuplicateInDrafts(List data, String composition) { + public static Set isFileNameDuplicateInDrafts( + List data, String composition, String targetEntity) { Set uniqueFilenames = new HashSet<>(); Set duplicateFilenames = new HashSet<>(); for (Map entity : data) { - List> attachments = (List>) entity.get(composition); + List> attachments = + AttachmentsHandlerUtils.fetchAttachments(targetEntity, entity, composition); if (attachments != null) { Iterator> iterator = attachments.iterator(); while (iterator.hasNext()) { diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java b/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java index 13d568e67..5a35fd04a 100644 --- a/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java @@ -530,7 +530,7 @@ public String renameAttachment( .build(); try (Response renameResponse = httpClient.newCall(request).execute()) { - if (renameResponse.code() != 200) { + if (!renameResponse.isSuccessful()) { System.out.println( "Rename Attachment failed in the " + facetName diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java b/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java index e961dc944..744b13183 100644 --- a/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java @@ -504,7 +504,7 @@ public String renameAttachment( .build(); try (Response renameResponse = httpClient.newCall(request).execute()) { - if (renameResponse.code() != 200) { + if (!renameResponse.isSuccessful()) { throw new IOException("Attachment was not renamed in section: " + facetName); } return "Renamed"; diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java b/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java index 0fcc83c38..3f97a8b7e 100644 --- a/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java @@ -604,8 +604,8 @@ void testRenameEntitiesWithUnsupportedCharacter() { counter = -1; // Reset counter for the next check response = api.saveEntityDraft(appUrl, entityName, srvpath, entityID); String expected = - "[{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 sample/1234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," - + "{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 reference1/234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," + "[{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 reference1/234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," + + "{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 sample/1234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," + "{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 footnote1/234\\n\\nRename the files and try again.\",\"numericSeverity\":3}]"; if (response.equals(expected)) { testStatus = true; @@ -673,7 +673,7 @@ void testRenameSingleDuplicate() { + "{\"code\":\"\",\"message\":\"The file(s) %s have been added multiple times. Please rename and try again.\",\"@Common.numericSeverity\":4}," + "{\"code\":\"\",\"message\":\"The file(s) %s have been added multiple times. Please rename and try again.\",\"@Common.numericSeverity\":4}" + "]}}", - name[0], name[1], name[2]); + name[1], name[0], name[2]); if (response.equals(expected)) { for (int i = 0; i < facet.length; i++) { // Attempt to rename again with a different name @@ -757,7 +757,7 @@ void testRenameEntitiesWithoutSDMRole() throws IOException { + // "\\n" + // - "\\t\\u2022 sample123\\n" + "\\t\\u2022 reference123\\n" + // "\\n" + // @@ -765,7 +765,7 @@ void testRenameEntitiesWithoutSDMRole() throws IOException { + // "\\n" + // - "\\t\\u2022 reference123\\n" + "\\t\\u2022 sample123\\n" + // "\\n" + // 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 03db40a13..dd0b4711f 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 @@ -9,8 +9,10 @@ import com.sap.cds.CdsData; import com.sap.cds.reflect.*; +import com.sap.cds.sdm.caching.CacheConfig; import com.sap.cds.sdm.handler.TokenHandler; import com.sap.cds.sdm.handler.applicationservice.SDMCreateAttachmentsHandler; +import com.sap.cds.sdm.handler.applicationservice.helper.AttachmentsHandlerUtils; import com.sap.cds.sdm.model.SDMCredentials; import com.sap.cds.sdm.persistence.DBQuery; import com.sap.cds.sdm.service.SDMService; @@ -24,7 +26,7 @@ import com.sap.cds.services.request.UserInfo; import java.io.IOException; import java.util.*; -import java.util.stream.Stream; +import org.ehcache.Cache; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -81,46 +83,58 @@ public void tearDown() { @Test public void testProcessBefore() throws IOException { - // Arrange the mock compositions scenario - List expectedCompositionNames = Arrays.asList("Name1", "Name2"); - - // Create a Stream of mocked CdsElement instances - Stream compositionsStream = Stream.of(cdsElement, cdsElement); - - when(context.getTarget().compositions()).thenReturn(compositionsStream); - when(cdsElement.getType()).thenReturn(cdsAssociationType); - when(cdsAssociationType.getTargetAspect()).thenReturn(Optional.of(targetAspect)); - when(cdsAssociationType.getTargetAspect().get().getQualifiedName()) - .thenReturn("sap.attachments.Attachments"); - when(cdsElement.getName()).thenReturn("Name1").thenReturn("Name2"); - - List dataList = new ArrayList<>(); - - // Act - handler.processBefore(context, dataList); - - // Assert that updateName was called with the compositions detected - for (String compositionName : expectedCompositionNames) { - verify(handler).updateName(context, dataList, compositionName); + try (MockedStatic attachmentsHandlerUtilsMocked = + mockStatic(AttachmentsHandlerUtils.class)) { + // Arrange the mock compositions scenario + Map expectedCompositionMapping = new HashMap<>(); + expectedCompositionMapping.put("Name1", "Name1"); + expectedCompositionMapping.put("Name2", "Name2"); + + // Mock AttachmentsHandlerUtils.getAttachmentPathMapping to return the expected mapping + attachmentsHandlerUtilsMocked + .when(() -> AttachmentsHandlerUtils.getAttachmentPathMapping(any(), any(), any())) + .thenReturn(expectedCompositionMapping); + + List dataList = new ArrayList<>(); + + // Act + handler.processBefore(context, dataList); + + // Assert that updateName was called with the compositions detected + for (Map.Entry entry : expectedCompositionMapping.entrySet()) { + verify(handler).updateName(context, dataList, entry.getKey(), entry.getValue()); + } } } @Test public void testUpdateNameWithDuplicateFilenames() throws IOException { - // Arrange - List data = new ArrayList<>(); - Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); - sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(duplicateFilenames); - - // Act - handler.updateName(context, data, "composition"); - - // Assert - verify(messages, times(1)) - .error( - "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Arrange + List data = new ArrayList<>(); + Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); + when(context.getMessages()).thenReturn(messages); + + // Mock the target entity + CdsEntity targetEntity = mock(CdsEntity.class); + when(targetEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getTarget()).thenReturn(targetEntity); + + sdmUtilsMockedStatic + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "compositionName", "TestEntity")) + .thenReturn(duplicateFilenames); + + // Act + handler.updateName(context, data, "compositionDefinition", "compositionName"); + + // Assert + verify(messages, times(1)) + .error( + "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); + } } @Test @@ -128,11 +142,11 @@ public void testUpdateNameWithEmptyData() throws IOException { // Arrange List data = new ArrayList<>(); sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "compositionName", "entity")) .thenReturn(Collections.emptySet()); // Act - handler.updateName(context, data, ""); + handler.updateName(context, data, "compositionDefinition", "compositionName"); // Assert verify(messages, never()).error(anyString()); @@ -141,32 +155,37 @@ public void testUpdateNameWithEmptyData() throws IOException { @Test public void testUpdateNameWithNoAttachments() throws IOException { - // Arrange - List data = new ArrayList<>(); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); - // Create an entity map without any attachments - Map entity = new HashMap<>(); + // Arrange + List data = new ArrayList<>(); - // Wrap the entity map in CdsData - CdsData cdsDataEntity = CdsData.create(entity); + // Create an entity map without any attachments + Map entity = new HashMap<>(); - // Add the CdsData entity to the data list - data.add(cdsDataEntity); + // Wrap the entity map in CdsData + CdsData cdsDataEntity = CdsData.create(entity); - // Mock utility methods - sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(Collections.emptySet()); + // Add the CdsData entity to the data list + data.add(cdsDataEntity); - // Act - handler.updateName(context, data, ""); + // Mock utility methods + sdmUtilsMockedStatic + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "compositionName", "entity")) + .thenReturn(Collections.emptySet()); - // Assert that no updateAttachments calls were made, as there are no attachments - verify(sdmService, never()).updateAttachments(any(), any(), any(), any(), anyBoolean()); + // Act + handler.updateName(context, data, "compositionDefinition", "compositionName"); - // Assert that no error or warning messages were logged - verify(messages, never()).error(anyString()); - verify(messages, never()).warn(anyString()); + // Assert that no updateAttachments calls were made, as there are no attachments + verify(sdmService, never()).updateAttachments(any(), any(), any(), any(), anyBoolean()); + + // Assert that no error or warning messages were logged + verify(messages, never()).error(anyString()); + verify(messages, never()).warn(anyString()); + } } // @Test @@ -383,88 +402,110 @@ public void testUpdateNameWithNoAttachments() throws IOException { // } @Test public void testUpdateNameWithEmptyFilename() throws IOException { - List data = new ArrayList<>(); - Map entity = new HashMap<>(); - List> attachments = new ArrayList<>(); - - Map attachment = new HashMap<>(); - attachment.put("ID", "test-id"); - attachment.put("fileName", null); // Empty filename - attachment.put("objectId", "test-object-id"); - attachments.add(attachment); - - // entity.put("attachments", attachments); - entity.put("composition", attachments); - - CdsData cdsDataEntity = CdsData.create(entity); // Wrap entity in CdsData - data.add(cdsDataEntity); // Add to data - - // Mock duplicate file name - sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(new HashSet<>()); - - // Mock attachment entity - CdsEntity attachmentDraftEntity = mock(CdsEntity.class); - when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); - when(context.getTarget()).thenReturn(attachmentDraftEntity); - when(context.getModel()).thenReturn(model); - - // Mock findEntity to return an optional containing attachmentDraftEntity - when(model.findEntity("some.qualified.Name" + "." + "composition")) - .thenReturn(Optional.of(attachmentDraftEntity)); - UserInfo userInfo = Mockito.mock(UserInfo.class); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.isSystemUser()).thenReturn(false); - // Mock authentication - when(context.getMessages()).thenReturn(messages); - when(context.getAuthenticationInfo()).thenReturn(authInfo); - when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); - when(jwtTokenInfo.getToken()).thenReturn("testJwtToken"); - - // Mock getObject - when(sdmService.getObject("test-object-id", mockCredentials, false)) - .thenReturn("fileInSDM.txt"); - - // Mock getSecondaryTypeProperties - Map secondaryTypeProperties = new HashMap<>(); - Map updatedSecondaryProperties = new HashMap<>(); - sdmUtilsMockedStatic - .when( - () -> - SDMUtils.getSecondaryTypeProperties(Optional.of(attachmentDraftEntity), attachment)) - .thenReturn(secondaryTypeProperties); - sdmUtilsMockedStatic - .when( - () -> - SDMUtils.getUpdatedSecondaryProperties( - Optional.of(attachmentDraftEntity), - attachment, - persistenceService, - secondaryTypeProperties, - updatedSecondaryProperties)) - .thenReturn(new HashMap<>()); - - // Mock restricted character - sdmUtilsMockedStatic - .when(() -> SDMUtils.isRestrictedCharactersInName("fileNameInRequest")) - .thenReturn(false); - - when(dbQuery.getAttachmentForID(attachmentDraftEntity, persistenceService, "test-id")) - .thenReturn(null); - - // When getPropertiesForID is called - when(dbQuery.getPropertiesForID( - attachmentDraftEntity, persistenceService, "test-id", secondaryTypeProperties)) - .thenReturn(updatedSecondaryProperties); - - // Act & Assert - ServiceException exception = - assertThrows( - ServiceException.class, () -> handler.updateName(context, data, "composition")); - - // Assert that the correct exception message is returned - assertEquals("Filename cannot be empty", exception.getMessage()); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + + Map attachment = new HashMap<>(); + attachment.put("ID", "test-id"); + attachment.put("fileName", null); // Empty filename + attachment.put("objectId", "test-object-id"); + attachments.add(attachment); + + // entity.put("attachments", attachments); + entity.put("composition", attachments); + + CdsData cdsDataEntity = CdsData.create(entity); // Wrap entity in CdsData + data.add(cdsDataEntity); // Add to data + + // Mock duplicate file name + sdmUtilsMockedStatic + .when( + () -> + SDMUtils.isFileNameDuplicateInDrafts( + data, "compositionName", "some.qualified.Name")) + .thenReturn(new HashSet<>()); + + // Mock AttachmentsHandlerUtils.fetchAttachments to return the attachment with null filename + try (MockedStatic attachmentsHandlerUtilsMocked = + mockStatic(AttachmentsHandlerUtils.class)) { + attachmentsHandlerUtilsMocked + .when( + () -> + AttachmentsHandlerUtils.fetchAttachments( + "some.qualified.Name", entity, "compositionName")) + .thenReturn(attachments); + + // Mock attachment entity + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + + // Mock findEntity to return an optional containing attachmentDraftEntity + when(model.findEntity("compositionDefinition")) + .thenReturn(Optional.of(attachmentDraftEntity)); + UserInfo userInfo = Mockito.mock(UserInfo.class); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.isSystemUser()).thenReturn(false); + // Mock authentication + when(context.getMessages()).thenReturn(messages); + when(context.getAuthenticationInfo()).thenReturn(authInfo); + when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); + when(jwtTokenInfo.getToken()).thenReturn("testJwtToken"); + + // Mock getObject + when(sdmService.getObject("test-object-id", mockCredentials, false)) + .thenReturn("fileInSDM.txt"); + + // Mock getSecondaryTypeProperties + Map secondaryTypeProperties = new HashMap<>(); + Map updatedSecondaryProperties = new HashMap<>(); + sdmUtilsMockedStatic + .when( + () -> + SDMUtils.getSecondaryTypeProperties( + Optional.of(attachmentDraftEntity), attachment)) + .thenReturn(secondaryTypeProperties); + sdmUtilsMockedStatic + .when( + () -> + SDMUtils.getUpdatedSecondaryProperties( + Optional.of(attachmentDraftEntity), + attachment, + persistenceService, + secondaryTypeProperties, + updatedSecondaryProperties)) + .thenReturn(new HashMap<>()); + + // Mock restricted character + sdmUtilsMockedStatic + .when(() -> SDMUtils.isRestrictedCharactersInName("fileNameInRequest")) + .thenReturn(false); + + when(dbQuery.getAttachmentForID(attachmentDraftEntity, persistenceService, "test-id")) + .thenReturn(null); + + // When getPropertiesForID is called + when(dbQuery.getPropertiesForID( + attachmentDraftEntity, persistenceService, "test-id", secondaryTypeProperties)) + .thenReturn(updatedSecondaryProperties); + + // Act & Assert + ServiceException exception = + assertThrows( + ServiceException.class, + () -> + handler.updateName(context, data, "compositionDefinition", "compositionName")); + + // Assert that the correct exception message is returned + assertEquals("Filename cannot be empty", exception.getMessage()); + } // Close AttachmentsHandlerUtils mock + } } // @Test 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 2c9cf5259..30a550235 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 @@ -9,9 +9,11 @@ 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.SDMUpdateAttachmentsHandler; +import com.sap.cds.sdm.handler.applicationservice.helper.AttachmentsHandlerUtils; import com.sap.cds.sdm.model.CmisDocument; import com.sap.cds.sdm.model.SDMCredentials; import com.sap.cds.sdm.persistence.DBQuery; @@ -26,7 +28,7 @@ import com.sap.cds.services.request.UserInfo; import java.io.IOException; import java.util.*; -import java.util.stream.Stream; +import org.ehcache.Cache; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.*; @@ -74,54 +76,69 @@ public void tearDown() { @Test public void testProcessBefore() throws IOException { - // Arrange - List expectedCompositionNames = Arrays.asList("Name1", "Name2"); - - // Simulate a stream of CdsElement instances returned from the mock target's compositions - Stream compositionsStream = Stream.of(cdsElement, cdsElement); + try (MockedStatic attachmentsHandlerUtilsMocked = + mockStatic(AttachmentsHandlerUtils.class); + MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Arrange the mock compositions scenario + Map expectedCompositionMapping = new HashMap<>(); + expectedCompositionMapping.put("Name1", "Name1"); + expectedCompositionMapping.put("Name2", "Name2"); + + // Mock context.getTarget() and context.getModel() + when(context.getTarget()).thenReturn(targetEntity); + when(targetEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getModel()).thenReturn(model); + when(model.findEntity(anyString())).thenReturn(Optional.of(targetEntity)); + + // Mock AttachmentsHandlerUtils.getAttachmentPathMapping to return the expected mapping + attachmentsHandlerUtilsMocked + .when(() -> AttachmentsHandlerUtils.getAttachmentPathMapping(any(), any(), any())) + .thenReturn(expectedCompositionMapping); + + List dataList = new ArrayList<>(); + + // Act + handler.processBefore(context, dataList); + + // Assert that updateName was called with the compositions detected + for (Map.Entry entry : expectedCompositionMapping.entrySet()) { + verify(handler).updateName(context, dataList, entry.getKey(), entry.getValue()); + } + } + } - // mock the target and model of the context - when(context.getTarget()).thenReturn(targetEntity); - when(targetEntity.compositions()).thenReturn(compositionsStream); - when(context.getModel()).thenReturn(model); + @Test + public void testRenameWithDuplicateFilenames() throws IOException { + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); - // Mock findEntity to return an optional containing attachmentDraftEntity - when(model.findEntity(anyString())).thenReturn(Optional.of(targetEntity)); + List data = new ArrayList<>(); + Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); + when(context.getMessages()).thenReturn(messages); - // Mock the elements and their associations - when(cdsElement.getType()).thenReturn(cdsAssociationType); - when(cdsAssociationType.getTargetAspect()).thenReturn(Optional.of(targetAspect)); - when(targetAspect.getQualifiedName()).thenReturn("sap.attachments.Attachments"); - when(cdsElement.getName()).thenReturn("Name1").thenReturn("Name2"); + // Mock the target entity + CdsEntity targetEntity = mock(CdsEntity.class); + when(targetEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getTarget()).thenReturn(targetEntity); - List dataList = new ArrayList<>(); + sdmUtilsMockedStatic = mockStatic(SDMUtils.class); + sdmUtilsMockedStatic + .when(() -> isFileNameDuplicateInDrafts(data, "compositionName", "TestEntity")) + .thenReturn(duplicateFilenames); - // Act - handler.processBefore(context, dataList); + handler.updateName(context, data, "compositionDefinition", "compositionName"); - // Assert that updateName was called with the compositions detected - for (String compositionName : expectedCompositionNames) { - verify(handler).updateName(context, dataList, compositionName); + verify(messages, times(1)) + .error( + "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); } } - @Test - public void testRenameWithDuplicateFilenames() throws IOException { - List data = new ArrayList<>(); - Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); - when(context.getMessages()).thenReturn(messages); - sdmUtilsMockedStatic = mockStatic(SDMUtils.class); - sdmUtilsMockedStatic - .when(() -> isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(duplicateFilenames); - - handler.updateName(context, data, "composition"); - - verify(messages, times(1)) - .error( - "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); - } - // @Test // public void testRenameWithUniqueFilenames() throws IOException { // List data = prepareMockAttachmentData("file1.txt"); @@ -212,62 +229,125 @@ public void testRenameWithDuplicateFilenames() throws IOException { @Test public void testRenameWithNoSDMRoles() throws IOException { - // Mock the data structure to simulate the attachments - List data = new ArrayList<>(); - Map entity = new HashMap<>(); - List> attachments = new ArrayList<>(); - Map attachment = spy(new HashMap<>()); - Map secondaryProperties = new HashMap<>(); - Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); - secondaryProperties.put("filename", "file1.txt"); - - CmisDocument document = new CmisDocument(); - document.setFileName("file1.txt"); - - attachment.put("fileName", "file1.txt"); - attachment.put("url", "objectId"); - attachment.put("ID", "test-id"); - attachments.add(attachment); - - entity.put("attachments", attachments); - CdsData mockCdsData = mock(CdsData.class); - when(mockCdsData.get("composition")).thenReturn(attachments); - data.add(mockCdsData); - - CdsEntity attachmentDraftEntity = mock(CdsEntity.class); - when(context.getTarget()).thenReturn(attachmentDraftEntity); - when(context.getModel()).thenReturn(model); - when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); - when(model.findEntity("some.qualified.Name.composition")) - .thenReturn(Optional.of(attachmentDraftEntity)); - when(context.getMessages()).thenReturn(messages); - UserInfo userInfo = Mockito.mock(UserInfo.class); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.isSystemUser()).thenReturn(false); - when(tokenHandler.getSDMCredentials()).thenReturn(mockCredentials); - when(dbQuery.getAttachmentForID( - any(CdsEntity.class), any(PersistenceService.class), anyString())) - .thenReturn("file123.txt"); - - when(sdmService.updateAttachments( - mockCredentials, - document, - secondaryProperties, - secondaryPropertiesWithInvalidDefinitions, - false)) - .thenReturn(403); // Forbidden + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class); + MockedStatic attachmentsMockStatic = + mockStatic(AttachmentsHandlerUtils.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Mock the data structure to simulate the attachments + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + Map attachment = spy(new HashMap<>()); + Map secondaryProperties = new HashMap<>(); + Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); + secondaryProperties.put("filename", "file1.txt"); - // Call the method - handler.updateName(context, data, "composition"); + CmisDocument document = new CmisDocument(); + document.setFileName("file1.txt"); - // Capture and assert the warning message - ArgumentCaptor warningCaptor = ArgumentCaptor.forClass(String.class); - verify(messages).warn(warningCaptor.capture()); - String warningMessage = warningCaptor.getValue(); + attachment.put("fileName", "file1.txt"); + attachment.put("url", "objectId"); + attachment.put("ID", "test-id"); + attachments.add(attachment); - String expectedMessage = - SDMConstants.noSDMRolesMessage(Collections.singletonList("file123.txt"), "update"); - assertEquals(expectedMessage, warningMessage); + entity.put("compositionName", attachments); + CdsData mockCdsData = mock(CdsData.class); + data.add(mockCdsData); + + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + when(model.findEntity("compositionDefinition")) + .thenReturn(Optional.of(attachmentDraftEntity)); + when(context.getMessages()).thenReturn(messages); + UserInfo userInfo = Mockito.mock(UserInfo.class); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.isSystemUser()).thenReturn(false); + when(tokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + when(dbQuery.getAttachmentForID( + any(CdsEntity.class), any(PersistenceService.class), anyString())) + .thenReturn("file123.txt"); + + when(sdmService.updateAttachments( + mockCredentials, + document, + secondaryProperties, + secondaryPropertiesWithInvalidDefinitions, + false)) + .thenReturn(403); // Forbidden + + // Mock AttachmentsHandlerUtils.fetchAttachments + attachmentsMockStatic + .when( + () -> + AttachmentsHandlerUtils.fetchAttachments( + anyString(), any(Map.class), eq("compositionName"))) + .thenReturn(attachments); + + // Mock SDMUtils methods + try (MockedStatic sdmUtilsMock = mockStatic(SDMUtils.class)) { + sdmUtilsMock + .when( + () -> + SDMUtils.isFileNameDuplicateInDrafts( + any(List.class), eq("compositionName"), anyString())) + .thenReturn(Collections.emptySet()); + + sdmUtilsMock + .when(() -> SDMUtils.getPropertyTitles(any(Optional.class), any(Map.class))) + .thenReturn(Collections.emptyMap()); + + sdmUtilsMock + .when( + () -> + SDMUtils.getSecondaryPropertiesWithInvalidDefinition( + any(Optional.class), any(Map.class))) + .thenReturn(Collections.emptyMap()); + + sdmUtilsMock + .when(() -> SDMUtils.getSecondaryTypeProperties(any(Optional.class), any(Map.class))) + .thenReturn(Collections.emptyMap()); + + sdmUtilsMock + .when( + () -> + SDMUtils.getUpdatedSecondaryProperties( + any(Optional.class), + any(Map.class), + any(PersistenceService.class), + any(Map.class), + any(Map.class))) + .thenReturn(secondaryProperties); + + sdmUtilsMock + .when(() -> SDMUtils.isRestrictedCharactersInName(anyString())) + .thenReturn(false); + + // Call the method + handler.updateName(context, data, "compositionDefinition", "compositionName"); + + // Capture and assert the warning message + ArgumentCaptor warningCaptor = ArgumentCaptor.forClass(String.class); + verify(messages).warn(warningCaptor.capture()); + String warningMessage = warningCaptor.getValue(); + + String expectedMessage = + SDMConstants.noSDMRolesMessage(Collections.singletonList("file123.txt"), "update"); + assertEquals(expectedMessage, warningMessage); + } + + // Capture and assert the warning message + ArgumentCaptor warningCaptor = ArgumentCaptor.forClass(String.class); + verify(messages).warn(warningCaptor.capture()); + String warningMessage = warningCaptor.getValue(); + + String expectedMessage = + SDMConstants.noSDMRolesMessage(Collections.singletonList("file123.txt"), "update"); + assertEquals(expectedMessage, warningMessage); + } } // @Test @@ -409,35 +489,41 @@ public void testRenameWithoutFileInSDM() throws IOException { @Test public void testRenameWithNoAttachments() throws IOException { - // Arrange - List data = new ArrayList<>(); - CdsEntity attachmentDraftEntity = mock(CdsEntity.class); - Map secondaryProperties = new HashMap<>(); - Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); - CmisDocument document = new CmisDocument(); - when(context.getTarget()).thenReturn(attachmentDraftEntity); - when(context.getModel()).thenReturn(model); - - when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); - - String expectedEntityName = "some.qualified.Name.attachments"; - when(model.findEntity(expectedEntityName)).thenReturn(Optional.of(attachmentDraftEntity)); - - CdsData mockCdsData = mock(CdsData.class); - when(mockCdsData.get("attachments")).thenReturn(null); - data.add(mockCdsData); - - // Act - handler.updateName(context, data, "attachments"); - - // Assert - verify(sdmService, never()) - .updateAttachments( - eq(mockCredentials), - eq(document), - eq(secondaryProperties), - eq(secondaryPropertiesWithInvalidDefinitions), - eq(false)); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Arrange + List data = new ArrayList<>(); + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + Map secondaryProperties = new HashMap<>(); + Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); + CmisDocument document = new CmisDocument(); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + + // Mock the correct entity name that the handler will look for + when(model.findEntity("compositionDefinition")) + .thenReturn(Optional.of(attachmentDraftEntity)); + + Map entity = new HashMap<>(); + CdsData cdsDataEntity = CdsData.create(entity); + data.add(cdsDataEntity); + + // Act + handler.updateName(context, data, "compositionDefinition", "compositionName"); + + // Assert + verify(sdmService, never()) + .updateAttachments( + eq(mockCredentials), + eq(document), + eq(secondaryProperties), + eq(secondaryPropertiesWithInvalidDefinitions), + eq(false)); + } } // @Test diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java index 0a3f98001..7ef6443d5 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java @@ -157,6 +157,9 @@ void testCreate_shouldCreateLink() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -249,6 +252,9 @@ void testCreate_ShouldThrowSpecifiedExceptionWhenMaxCountReached() throws IOExce when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -307,6 +313,9 @@ void testCreate_ShouldThrowDefaultExceptionWhenMaxCountReached() throws IOExcept when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -365,6 +374,9 @@ void testCreate_ShouldThrowExceptionWhenRestrictedCharacterInLinkName() throws I when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("test/URL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -425,6 +437,9 @@ void testCreate_ThrowsServiceExceptionOnDuplicateFile() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -486,6 +501,9 @@ void testCreate_ThrowsServiceException_WhenCreateDocumentThrowsException() throw when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -554,6 +572,9 @@ void testCreate_ThrowsServiceExceptionOnDuplicateStatus() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -623,6 +644,9 @@ void testCreate_ThrowsServiceExceptionOnFailStatus() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -691,6 +715,9 @@ void testCreate_ThrowsServiceExceptionOnUnauthorizedStatus() throws IOException when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java index fa7f35387..dedb2f5ec 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java @@ -71,7 +71,6 @@ private void setUp() { @Test public void testIsFileNameDuplicateInDrafts() { List data = new ArrayList<>(); - CdsData mockCdsData = mock(CdsData.class); Map entity = new HashMap<>(); List> attachments = new ArrayList<>(); Map attachment1 = new HashMap<>(); @@ -82,11 +81,15 @@ public void testIsFileNameDuplicateInDrafts() { attachment2.put("repositoryId", "repo1"); attachments.add(attachment1); attachments.add(attachment2); - entity.put("attachments", attachments); - when(mockCdsData.get("attachments")).thenReturn(attachments); // Correctly mock get method - data.add(mockCdsData); - Set duplicateFilenames = SDMUtils.isFileNameDuplicateInDrafts(data, "attachments"); + // Create the nested structure that fetchAttachments expects + Map entityData = new HashMap<>(); + entityData.put("attachmentCompositionName", attachments); + entity.put("entity", entityData); + data.add(CdsData.create(entity)); + + Set duplicateFilenames = + SDMUtils.isFileNameDuplicateInDrafts(data, "attachmentCompositionName", "entity"); assertTrue(duplicateFilenames.contains("file1.txt")); } @@ -640,7 +643,6 @@ void testElementWithoutAnnotation() { void testElementWithAnnotation() { CdsEntity entity = mock(CdsEntity.class); CdsElement element = mock(CdsElement.class); - @SuppressWarnings("unchecked") CdsAnnotation annotation = mock(CdsAnnotation.class); when(annotation.getValue()).thenReturn("name");
This method creates an SDMAttachmentsReader instance to traverse the entity's structure and + * identify all paths that lead to attachment entities within the CDS model. It uses an + * SDMAssociationCascader to handle cascading through entity associations and compositions to find + * nested attachment relationships. + * + * @param model the CDS model containing entity definitions and relationships + * @param entity the target CDS entity to analyze for attachment paths + * @param persistenceService the persistence service used for data access operations + * @return a list of strings representing paths to attachment entities, or an empty list if no + * attachments are found or if an error occurs during processing + */ + public static List getAttachmentEntityPaths( + CdsModel model, CdsEntity entity, PersistenceService persistenceService) { + try { + SDMAssociationCascader cascader = new SDMAssociationCascader(); + SDMAttachmentsReader reader = new SDMAttachmentsReader(cascader, persistenceService); + return reader.getAttachmentEntityPaths(model, entity); + } catch (Exception e) { + return new ArrayList<>(); + } + } + + /** + * Creates a mapping of attachment entity paths to their corresponding actual paths within the CDS + * model. + * + * This method analyzes both direct and nested attachment compositions within the given entity. + * It processes direct attachments that are immediate compositions of the entity, and also + * traverses nested compositions to find attachments in related entities. The resulting mapping + * provides a translation between logical attachment paths and their actual implementation paths. + * + * @param model the CDS model containing entity definitions and relationships + * @param entity the target CDS entity to analyze for attachment path mappings + * @param persistenceService the persistence service used for data access operations + * @return a map where keys are attachment entity paths and values are the corresponding actual + * paths, or an empty map if no attachments are found or if an error occurs during processing + */ + public static Map getAttachmentPathMapping( + CdsModel model, CdsEntity entity, PersistenceService persistenceService) { + try { + Map pathMapping = new HashMap<>(); + SDMAssociationCascader cascader = new SDMAssociationCascader(); + SDMAttachmentsReader reader = new SDMAttachmentsReader(cascader, persistenceService); + + // Process direct attachments + entity + .compositions() + .forEach( + composition -> processDirectAttachmentComposition(entity, pathMapping, composition)); + + // Process nested attachments + entity + .compositions() + .forEach( + composition -> + processNestedAttachmentComposition( + model, entity, reader, pathMapping, composition)); + + return pathMapping; + } catch (Exception e) { + logger.error(SDMConstants.FETCH_ATTACHMENT_COMPOSITION_ERROR, e.getMessage()); + return new HashMap<>(); + } + } + + private static void processDirectAttachmentComposition( + CdsEntity entity, Map pathMapping, Object composition) { + String compositionName = ((com.sap.cds.reflect.CdsElement) composition).getName(); + if (((com.sap.cds.reflect.CdsElement) composition).getType().isAssociation()) { + CdsAssociationType associationType = + (CdsAssociationType) ((com.sap.cds.reflect.CdsElement) composition).getType(); + String targetAspect = + associationType.getTargetAspect().isPresent() + ? associationType.getTargetAspect().get().getQualifiedName() + : null; + + if (isDirectAttachmentTargetAspect(targetAspect)) { + String serviceName = entity.getQualifiedName().split("\\.")[0]; + String entityName = entity.getName(); + String directPath = serviceName + "." + entityName + "." + compositionName; + pathMapping.put(directPath, directPath); + } + } + } + + private static void processNestedAttachmentComposition( + CdsModel model, + CdsEntity entity, + SDMAttachmentsReader reader, + Map pathMapping, + Object composition) { + String compositionName = ((com.sap.cds.reflect.CdsElement) composition).getName(); + String compositionTargetEntityName = ""; + + if (((com.sap.cds.reflect.CdsElement) composition).getType().isAssociation()) { + CdsAssociationType associationType = + (CdsAssociationType) ((com.sap.cds.reflect.CdsElement) composition).getType(); + String targetAspect = + associationType.getTargetAspect().isPresent() + ? associationType.getTargetAspect().get().getQualifiedName() + : null; + + if (isDirectAttachmentTargetAspect(targetAspect)) { + return; // Skip direct attachment compositions + } + + compositionTargetEntityName = associationType.getTarget().getQualifiedName(); + } + + processCompositionTargetEntity( + model, entity, reader, pathMapping, compositionName, compositionTargetEntityName); + } + + private static void processCompositionTargetEntity( + CdsModel model, + CdsEntity entity, + SDMAttachmentsReader reader, + Map pathMapping, + String compositionName, + String compositionTargetEntityName) { + if (!compositionTargetEntityName.isEmpty()) { + Optional targetEntityOpt = model.findEntity(compositionTargetEntityName); + if (targetEntityOpt.isPresent()) { + CdsEntity targetEntity = targetEntityOpt.get(); + List attachmentPaths = reader.getAttachmentEntityPaths(model, targetEntity); + processAttachmentPaths(entity, pathMapping, compositionName, targetEntity, attachmentPaths); + } + } + } + + private static void processAttachmentPaths( + CdsEntity entity, + Map pathMapping, + String compositionName, + CdsEntity targetEntity, + List attachmentPaths) { + for (String attachmentPath : attachmentPaths) { + String entityPath = buildEntityPath(entity, targetEntity, attachmentPath); + String actualPath = buildActualPath(entity, compositionName, attachmentPath); + + if (entityPath != null && actualPath != null) { + pathMapping.put(entityPath, actualPath); + } + } + } + + private static boolean isDirectAttachmentTargetAspect(String targetAspect) { + return targetAspect != null && targetAspect.equalsIgnoreCase("sap.attachments.Attachments"); + } + + /** + * Fetches attachment data from a nested entity structure based on the target entity and + * composition name. + * + * This method processes the target entity path to extract the entity name, wraps the provided + * entity data with a parent structure, and then searches for attachments within the nested + * structure. It parses the attachment composition name to identify both the attachment key (e.g., + * "attachments") and the parent key (e.g., "chapters") for precise attachment location. + * + * @param targetEntity the qualified name of the target entity (e.g., "ServiceName.EntityName") + * @param entity the entity data structure containing potential attachment information + * @param attachmentCompositionName the composition path to the attachments (e.g., + * "chapters.attachments") + * @return a list of maps representing attachment objects found in the entity structure, or an + * empty list if no attachments are found + */ + public static List> fetchAttachments( + String targetEntity, Map entity, String attachmentCompositionName) { + String[] targetEntityPath = targetEntity.split("\\."); + targetEntity = targetEntityPath[targetEntityPath.length - 1]; + entity = AttachmentsHandlerUtils.wrapEntityWithParent(entity, targetEntity.toLowerCase()); + String[] compositionParts = attachmentCompositionName.split("\\."); + String attachmentKeyFromComposition = + compositionParts[compositionParts.length - 1]; // Last part (e.g., "attachments") + String parentKeyFromComposition = + compositionParts.length >= 2 + ? compositionParts[compositionParts.length - 2].toLowerCase() + : null; // Second last part (e.g., "chapters") + + // Find all attachment arrays in the nested entity structure + return AttachmentsHandlerUtils.findNestedAttachments( + entity, attachmentKeyFromComposition, parentKeyFromComposition); + } + + private static List> findNestedAttachments( + Map entity, String attachmentKey, String parentKey) { + return findNestedAttachments(entity, attachmentKey, parentKey, null); + } + + private static String buildEntityPath( + CdsEntity parentEntity, CdsEntity targetEntity, String attachmentPath) { + try { + String[] pathParts = attachmentPath.split("\\."); + if (pathParts.length >= 3) { + // Get the service name (first part) + String serviceName = pathParts[0]; + + // Get the target entity name (without service prefix) + String targetEntityName = targetEntity.getName(); + + // Get the attachment part (last part) + String attachmentPart = pathParts[pathParts.length - 1]; + + // Build the entity path: ServiceName.EntityName.attachments + return serviceName + "." + targetEntityName + "." + attachmentPart; + } + } catch (Exception e) { + logger.warn(SDMConstants.FETCH_ATTACHMENT_COMPOSITION_ERROR, e.getMessage()); + } + return null; + } + + private static String buildActualPath( + CdsEntity parentEntity, String compositionPropertyName, String attachmentPath) { + try { + String[] pathParts = attachmentPath.split("\\."); + if (pathParts.length >= 3) { + // Get the service name (first part) + String serviceName = pathParts[0]; + + // Replace the entity name with the composition property name + // Keep the attachment part (last part) + String attachmentPart = pathParts[pathParts.length - 1]; + + // Build the new path: ServiceName.compositionPropertyName.attachments + return serviceName + "." + compositionPropertyName + "." + attachmentPart; + } + } catch (Exception e) { + logger.warn(SDMConstants.FETCH_ATTACHMENT_COMPOSITION_ERROR, e.getMessage()); + } + return null; + } + + private static List> findNestedAttachments( + Map entity, String attachmentKey, String parentKey, String currentParentKey) { + List> result = new ArrayList<>(); + + for (Map.Entry entry : entity.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + // If we found the attachment key + if (attachmentKey.equals(key) && value instanceof List) { + result.addAll(processAttachmentKey(value, key, parentKey, currentParentKey)); + } + // Recursively search in nested objects + else if (value instanceof Map) { + result.addAll(processNestedMap(value, key, attachmentKey, parentKey)); + } + // Recursively search in lists + else if (value instanceof List) { + result.addAll(processNestedList(value, key, attachmentKey, parentKey)); + } + } + + return result; + } + + private static List> processAttachmentKey( + Object value, String key, String parentKey, String currentParentKey) { + List> result = new ArrayList<>(); + + // Check if the parent matches (if parentKey is specified) + if (parentKey == null || isCorrectParentContext(currentParentKey, parentKey)) { + try { + List> attachments = (List>) value; + result.addAll(attachments); + } catch (ClassCastException e) { + logger.warn(SDMConstants.FETCH_ATTACHMENT_COMPOSITION_ERROR, e.getMessage()); + } + } + + return result; + } + + private static List> processNestedMap( + Object value, String key, String attachmentKey, String parentKey) { + List> result = new ArrayList<>(); + + try { + Map nestedMap = (Map) value; + result.addAll(findNestedAttachments(nestedMap, attachmentKey, parentKey, key)); + } catch (ClassCastException e) { + logger.warn(SDMConstants.FETCH_ATTACHMENT_COMPOSITION_ERROR, e.getMessage()); + } + + return result; + } + + private static List> processNestedList( + Object value, String key, String attachmentKey, String parentKey) { + List> result = new ArrayList<>(); + + try { + List> list = (List>) value; + for (Object item : list) { + if (item instanceof Map) { + Map itemMap = (Map) item; + result.addAll(findNestedAttachments(itemMap, attachmentKey, parentKey, key)); + } + } + } catch (ClassCastException e) { + logger.warn(SDMConstants.FETCH_ATTACHMENT_COMPOSITION_ERROR, e.getMessage()); + } + + return result; + } + + private static boolean isCorrectParentContext(String currentParentKey, String expectedParentKey) { + // If no specific parent is expected, any context is valid + if (expectedParentKey == null) { + return true; + } + + // If we're at root level (no current parent) and expecting a specific parent, no match + if (currentParentKey == null) { + return false; + } + + // Check if the current parent matches the expected parent + return expectedParentKey.equals(currentParentKey); + } + + /** + * Wraps an entity data structure with a parent container using the specified target entity name. + * + * This utility method creates a new map with the target entity name as the key and the + * provided entity data as the value. This is necessary because the root of the target entity in + * the CdsData object is not mentioned explicitly, and hence interferes with the recursive + * fetching of attachment compositions. + * + * @param root the entity data structure to be wrapped + * @param targetEntity the name to use as the parent key for wrapping the entity data + * @return a new map containing the target entity name as key and the root entity data as value + */ + public static Map wrapEntityWithParent( + Map root, String targetEntity) { + Map wrapper = new HashMap<>(); + wrapper.put(targetEntity, root); + return wrapper; + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMApplicationHandlerHelper.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMApplicationHandlerHelper.java new file mode 100644 index 000000000..6bb0824a0 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMApplicationHandlerHelper.java @@ -0,0 +1,26 @@ +package com.sap.cds.sdm.handler.common; + +import com.sap.cds.reflect.CdsStructuredType; + +/** + * The class {@link SDMApplicationHandlerHelper} provides helper methods for the SDM attachment + * application handlers. + */ +public final class SDMApplicationHandlerHelper { + private static final String ANNOTATION_IS_MEDIA_DATA = "_is_media_data"; + + /** + * Checks if the entity is a media entity. A media entity is an entity that is annotated with the + * annotation "_is_media_data". + * + * @param baseEntity The entity to check + * @return true if the entity is a media entity, false otherwise + */ + public static boolean isMediaEntity(CdsStructuredType baseEntity) { + return baseEntity.getAnnotationValue(ANNOTATION_IS_MEDIA_DATA, false); + } + + private SDMApplicationHandlerHelper() { + // avoid instantiation + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationCascader.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationCascader.java new file mode 100644 index 000000000..48e8c2734 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationCascader.java @@ -0,0 +1,97 @@ +package com.sap.cds.sdm.handler.common; + +import com.sap.cds.reflect.CdsAssociationType; +import com.sap.cds.reflect.CdsElementDefinition; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.reflect.CdsModel; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +/** + * The class {@link SDMAssociationCascader} is used to find entity paths to all media resource + * entities for a given data model. The path information is returned in a node tree which starts + * from the given entity. Only composition associations are considered. + */ +public class SDMAssociationCascader { + + public SDMNodeTree findEntityPath(CdsModel model, CdsEntity entity) { + var firstList = new LinkedList(); + var internalResultList = + getAttachmentAssociationPath( + model, entity, "", firstList, new ArrayList<>(List.of(entity.getQualifiedName()))); + + var rootTree = new SDMNodeTree(new SDMAssociationIdentifier("", entity.getQualifiedName())); + internalResultList.forEach(rootTree::addPath); + return rootTree; + } + + private List> getAttachmentAssociationPath( + CdsModel model, + CdsEntity entity, + String associationName, + LinkedList firstList, + List processedEntities) { + var internalResultList = new ArrayList>(); + var currentList = new AtomicReference>(); + var localProcessEntities = new ArrayList(); + currentList.set(new LinkedList<>()); + + var isMediaEntity = SDMApplicationHandlerHelper.isMediaEntity(entity); + if (isMediaEntity) { + var identifier = new SDMAssociationIdentifier(associationName, entity.getQualifiedName()); + firstList.addLast(identifier); + } + + if (isMediaEntity) { + internalResultList.add(firstList); + return internalResultList; + } + + Map associations = + entity + .elements() + .filter( + element -> + element.getType().isAssociation() + && element.getType().as(CdsAssociationType.class).isComposition()) + .collect( + Collectors.toMap( + CdsElementDefinition::getName, + element -> element.getType().as(CdsAssociationType.class).getTarget())); + + if (associations.isEmpty()) { + return internalResultList; + } + + var newListNeeded = false; + for (var associatedElement : associations.entrySet()) { + if (!processedEntities.contains(associatedElement.getValue().getQualifiedName())) { + if (newListNeeded) { + currentList.set(new LinkedList<>()); + currentList.get().addAll(firstList); + processedEntities = localProcessEntities; + } else { + firstList.add(new SDMAssociationIdentifier(associationName, entity.getQualifiedName())); + currentList.get().addAll(firstList); + localProcessEntities = new ArrayList<>(processedEntities); + } + processedEntities.add(associatedElement.getValue().getQualifiedName()); + newListNeeded = true; + var result = + getAttachmentAssociationPath( + model, + associatedElement.getValue(), + associatedElement.getKey(), + currentList.get(), + processedEntities); + internalResultList.addAll(result); + } + } + + return internalResultList; + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationIdentifier.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationIdentifier.java new file mode 100644 index 000000000..7ee462949 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationIdentifier.java @@ -0,0 +1,10 @@ +package com.sap.cds.sdm.handler.common; + +/** + * This record is a simple data class that holds the association name and the full entity name for + * SDM attachment processing. + * + * @param associationName the association name + * @param fullEntityName the full entity name + */ +record SDMAssociationIdentifier(String associationName, String fullEntityName) {} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAttachmentsReader.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAttachmentsReader.java new file mode 100644 index 000000000..1164cdf23 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAttachmentsReader.java @@ -0,0 +1,113 @@ +package com.sap.cds.sdm.handler.common; + +import static java.util.Objects.requireNonNull; + +import com.sap.cds.Result; +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments; +import com.sap.cds.ql.CQL; +import com.sap.cds.ql.Expand; +import com.sap.cds.ql.Select; +import com.sap.cds.ql.StructuredType; +import com.sap.cds.ql.cqn.CqnFilterableStatement; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.reflect.CdsModel; +import com.sap.cds.services.persistence.PersistenceService; +import java.util.ArrayList; +import java.util.List; + +/** + * The class {@link SDMAttachmentsReader} is used to deep read attachments from the database for a + * determined path from the given entity to the media entity. The class uses the {@link + * SDMAssociationCascader} to find the entity path. + * + * The returned data is deep including the path structure to the media entity. + */ +public class SDMAttachmentsReader { + + private final SDMAssociationCascader cascader; + private final PersistenceService persistence; + + public SDMAttachmentsReader(SDMAssociationCascader cascader, PersistenceService persistence) { + this.cascader = requireNonNull(cascader, "cascader must not be null"); + this.persistence = requireNonNull(persistence, "persistence must not be null"); + } + + public List readAttachments( + CdsModel model, CdsEntity entity, CqnFilterableStatement statement) { + + SDMNodeTree nodePath = cascader.findEntityPath(model, entity); + List> expandList = buildExpandList(nodePath); + + Select> select; + if (!expandList.isEmpty()) { + select = Select.from(statement.ref()).columns(expandList); + } else { + select = Select.from(statement.ref()).columns(StructuredType::_all); + } + + if (statement.where().isPresent()) { + select.where(statement.where().get()); + } + + Result result = persistence.run(select); + return result.listOf(Attachments.class); + } + + public List getAttachmentEntityPaths(CdsModel model, CdsEntity entity) { + SDMNodeTree nodePath = cascader.findEntityPath(model, entity); + + List attachmentPaths = new ArrayList<>(); + + if (nodePath != null) { + collectAttachmentPaths(nodePath, attachmentPaths, model); + } + return attachmentPaths; + } + + private void collectAttachmentPaths( + SDMNodeTree node, List attachmentPaths, CdsModel model) { + String entityName = node.getIdentifier().fullEntityName(); + + // Check if this entity is an attachment entity + if (isAttachmentEntity(model, entityName)) { + attachmentPaths.add(entityName); + } + + // Recursively check children + for (SDMNodeTree child : node.getChildren()) { + collectAttachmentPaths(child, attachmentPaths, model); + } + } + + private boolean isAttachmentEntity(CdsModel model, String entityName) { + var entityOpt = model.findEntity(entityName); + if (!entityOpt.isPresent()) { + return false; + } + + CdsEntity entity = entityOpt.get(); + // Check if this entity has the @_is_media_data annotation (indicating attachment entity) + return entity.getAnnotationValue("_is_media_data", false); + } + + private List> buildExpandList(SDMNodeTree root) { + List> expandResultList = new ArrayList<>(); + root.getChildren() + .forEach( + child -> { + Expand> expand = buildExpandFromTree(child); + expandResultList.add(expand); + }); + + return expandResultList; + } + + private Expand> buildExpandFromTree(SDMNodeTree node) { + if (node.getChildren().isEmpty()) { + return CQL.to(node.getIdentifier().associationName()).expand(); + } else { + return CQL.to(node.getIdentifier().associationName()) + .expand(node.getChildren().stream().map(this::buildExpandFromTree).toList()); + } + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMNodeTree.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMNodeTree.java new file mode 100644 index 000000000..71611bef2 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMNodeTree.java @@ -0,0 +1,65 @@ +package com.sap.cds.sdm.handler.common; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * The class {@link SDMNodeTree} is a tree data structure that holds the SDM association identifier + * and its children for attachment processing. + */ +class SDMNodeTree { + + private final SDMAssociationIdentifier identifier; + private final List children = new ArrayList<>(); + + SDMNodeTree(SDMAssociationIdentifier identifier) { + this.identifier = identifier; + } + + void addPath(List path) { + var currentIdentifierOptional = + path.stream() + .filter(entry -> entry.fullEntityName().equals(identifier.fullEntityName())) + .findAny(); + if (currentIdentifierOptional.isEmpty()) { + return; + } + var currentNode = this; + var index = path.indexOf(currentIdentifierOptional.get()); + if (index == path.size() - 1) { + return; + } + for (var i = index + 1; i < path.size(); i++) { + var pathEntry = path.get(i); + currentNode = currentNode.getChildOrNew(pathEntry); + } + } + + private SDMNodeTree getChildOrNew(SDMAssociationIdentifier identifier) { + var childOptional = + children.stream() + .filter(child -> child.identifier.fullEntityName().equals(identifier.fullEntityName())) + .findAny(); + if (childOptional.isPresent()) { + return childOptional.get(); + } else { + SDMNodeTree child = new SDMNodeTree(identifier); + children.add(child); + return child; + } + } + + SDMAssociationIdentifier getIdentifier() { + return identifier; + } + + List getChildren() { + return Collections.unmodifiableList(children); + } + + @Override + public String toString() { + return "SDMNodeTree{" + "identifier=" + identifier + ", children=" + children + '}'; + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java index c5c95242c..1098a1eea 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java @@ -188,9 +188,9 @@ private String getUpIdKey(CdsEntity attachmentDraftEntity) { if (upAssociation.isPresent()) { CdsElement association = upAssociation.get(); // get association type - CdsAssociationType assocType = association.getType(); + CdsAssociationType associationType = association.getType(); // get the refs of the association - List fkElements = assocType.refs().map(ref -> "up__" + ref.path()).toList(); + List fkElements = associationType.refs().map(ref -> "up__" + ref.path()).toList(); upIdKey = fkElements.get(0); } return upIdKey; diff --git a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java index b959d24e3..4cedef71b 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java @@ -1,5 +1,7 @@ package com.sap.cds.sdm.service.handler; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.sap.cds.Result; import com.sap.cds.feature.attachments.service.AttachmentService; import com.sap.cds.ql.Insert; @@ -136,9 +138,7 @@ private void createLink(EventContext context) throws IOException { String upIdKey = attachmentDraftEntity.isPresent() ? getUpIdKey(attachmentDraftEntity.get()) : "up__ID"; CqnSelect select = (CqnSelect) context.get("cqn"); - CqnAnalyzer cqnAnalyzer = CqnAnalyzer.create(cdsModel); - String id = upIdKey.replaceFirst("^up__", ""); - String upID = cqnAnalyzer.analyze(select).rootKeys().get(id).toString(); + String upID = fetchUPIDFromCQN(select); String filenameInRequest = context.get("name").toString(); Result result = @@ -214,9 +214,9 @@ private String getUpIdKey(CdsEntity attachmentDraftEntity) { if (upAssociation.isPresent()) { CdsElement association = upAssociation.get(); // get association type - CdsAssociationType assocType = association.getType(); + CdsAssociationType associationType = association.getType(); // get the refs of the association - List fkElements = assocType.refs().map(ref -> "up__" + ref.path()).toList(); + List fkElements = associationType.refs().map(ref -> "up__" + ref.path()).toList(); upIdKey = fkElements.get(0); } return upIdKey; @@ -308,14 +308,45 @@ private void handleCreateLinkResult( + ":" + context.getTarget()); - var insert = Insert.into(context.getTarget().getQualifiedName()).entry(updatedFields); - for (DraftService draftS : draftService) { - // Process each draftService object - if (context.getTarget().getQualifiedName().contains(draftS.getName())) { - draftS.newDraft(insert); + try { + var insert = Insert.into(context.getTarget().getQualifiedName()).entry(updatedFields); + for (DraftService draftS : draftService) { + if (context.getTarget().getQualifiedName().contains(draftS.getName())) { + draftS.newDraft(insert); + } } + } catch (Exception e) { + logger.info("Exception in insert : " + e.getMessage()); } context.setCompleted(); } } + + private String fetchUPIDFromCQN(CqnSelect select) { + try { + String upID = null; + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = mapper.readTree(select.toString()); + JsonNode refArray = root.path("SELECT").path("from").path("ref"); + JsonNode secondLast = refArray.get(refArray.size() - 2); + JsonNode whereArray = secondLast.path("where"); + for (int i = 0; i < whereArray.size(); i++) { + JsonNode node = whereArray.get(i); + if (node.has("ref") + && node.get("ref").isArray() + && node.get("ref").get(0).asText().equals("ID")) { + JsonNode valNode = whereArray.get(i + 2); + upID = valNode.path("val").asText(); + break; + } + } + if (upID == null) { + throw new ServiceException(SDMConstants.ENTITY_PROCESSING_ERROR_LINK); + } + return upID; + } catch (Exception e) { + logger.error(SDMConstants.ENTITY_PROCESSING_ERROR_LINK, e); + throw new ServiceException(SDMConstants.ENTITY_PROCESSING_ERROR_LINK, e); + } + } } diff --git a/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java b/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java index bf073caba..c6896b2de 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java +++ b/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java @@ -6,6 +6,7 @@ import com.sap.cds.reflect.CdsEntity; import com.sap.cds.sdm.caching.CacheConfig; import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.sdm.handler.applicationservice.helper.AttachmentsHandlerUtils; import com.sap.cds.sdm.model.AttachmentInfo; import com.sap.cds.services.persistence.PersistenceService; import java.io.IOException; @@ -32,11 +33,13 @@ private SDMUtils() { // Doesn't do anything } - public static Set isFileNameDuplicateInDrafts(List data, String composition) { + public static Set isFileNameDuplicateInDrafts( + List data, String composition, String targetEntity) { Set uniqueFilenames = new HashSet<>(); Set duplicateFilenames = new HashSet<>(); for (Map entity : data) { - List> attachments = (List>) entity.get(composition); + List> attachments = + AttachmentsHandlerUtils.fetchAttachments(targetEntity, entity, composition); if (attachments != null) { Iterator> iterator = attachments.iterator(); while (iterator.hasNext()) { diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java b/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java index 13d568e67..5a35fd04a 100644 --- a/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java @@ -530,7 +530,7 @@ public String renameAttachment( .build(); try (Response renameResponse = httpClient.newCall(request).execute()) { - if (renameResponse.code() != 200) { + if (!renameResponse.isSuccessful()) { System.out.println( "Rename Attachment failed in the " + facetName diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java b/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java index e961dc944..744b13183 100644 --- a/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java @@ -504,7 +504,7 @@ public String renameAttachment( .build(); try (Response renameResponse = httpClient.newCall(request).execute()) { - if (renameResponse.code() != 200) { + if (!renameResponse.isSuccessful()) { throw new IOException("Attachment was not renamed in section: " + facetName); } return "Renamed"; diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java b/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java index 0fcc83c38..3f97a8b7e 100644 --- a/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java @@ -604,8 +604,8 @@ void testRenameEntitiesWithUnsupportedCharacter() { counter = -1; // Reset counter for the next check response = api.saveEntityDraft(appUrl, entityName, srvpath, entityID); String expected = - "[{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 sample/1234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," - + "{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 reference1/234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," + "[{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 reference1/234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," + + "{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 sample/1234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," + "{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 footnote1/234\\n\\nRename the files and try again.\",\"numericSeverity\":3}]"; if (response.equals(expected)) { testStatus = true; @@ -673,7 +673,7 @@ void testRenameSingleDuplicate() { + "{\"code\":\"\",\"message\":\"The file(s) %s have been added multiple times. Please rename and try again.\",\"@Common.numericSeverity\":4}," + "{\"code\":\"\",\"message\":\"The file(s) %s have been added multiple times. Please rename and try again.\",\"@Common.numericSeverity\":4}" + "]}}", - name[0], name[1], name[2]); + name[1], name[0], name[2]); if (response.equals(expected)) { for (int i = 0; i < facet.length; i++) { // Attempt to rename again with a different name @@ -757,7 +757,7 @@ void testRenameEntitiesWithoutSDMRole() throws IOException { + // "\\n" + // - "\\t\\u2022 sample123\\n" + "\\t\\u2022 reference123\\n" + // "\\n" + // @@ -765,7 +765,7 @@ void testRenameEntitiesWithoutSDMRole() throws IOException { + // "\\n" + // - "\\t\\u2022 reference123\\n" + "\\t\\u2022 sample123\\n" + // "\\n" + // 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 03db40a13..dd0b4711f 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 @@ -9,8 +9,10 @@ import com.sap.cds.CdsData; import com.sap.cds.reflect.*; +import com.sap.cds.sdm.caching.CacheConfig; import com.sap.cds.sdm.handler.TokenHandler; import com.sap.cds.sdm.handler.applicationservice.SDMCreateAttachmentsHandler; +import com.sap.cds.sdm.handler.applicationservice.helper.AttachmentsHandlerUtils; import com.sap.cds.sdm.model.SDMCredentials; import com.sap.cds.sdm.persistence.DBQuery; import com.sap.cds.sdm.service.SDMService; @@ -24,7 +26,7 @@ import com.sap.cds.services.request.UserInfo; import java.io.IOException; import java.util.*; -import java.util.stream.Stream; +import org.ehcache.Cache; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -81,46 +83,58 @@ public void tearDown() { @Test public void testProcessBefore() throws IOException { - // Arrange the mock compositions scenario - List expectedCompositionNames = Arrays.asList("Name1", "Name2"); - - // Create a Stream of mocked CdsElement instances - Stream compositionsStream = Stream.of(cdsElement, cdsElement); - - when(context.getTarget().compositions()).thenReturn(compositionsStream); - when(cdsElement.getType()).thenReturn(cdsAssociationType); - when(cdsAssociationType.getTargetAspect()).thenReturn(Optional.of(targetAspect)); - when(cdsAssociationType.getTargetAspect().get().getQualifiedName()) - .thenReturn("sap.attachments.Attachments"); - when(cdsElement.getName()).thenReturn("Name1").thenReturn("Name2"); - - List dataList = new ArrayList<>(); - - // Act - handler.processBefore(context, dataList); - - // Assert that updateName was called with the compositions detected - for (String compositionName : expectedCompositionNames) { - verify(handler).updateName(context, dataList, compositionName); + try (MockedStatic attachmentsHandlerUtilsMocked = + mockStatic(AttachmentsHandlerUtils.class)) { + // Arrange the mock compositions scenario + Map expectedCompositionMapping = new HashMap<>(); + expectedCompositionMapping.put("Name1", "Name1"); + expectedCompositionMapping.put("Name2", "Name2"); + + // Mock AttachmentsHandlerUtils.getAttachmentPathMapping to return the expected mapping + attachmentsHandlerUtilsMocked + .when(() -> AttachmentsHandlerUtils.getAttachmentPathMapping(any(), any(), any())) + .thenReturn(expectedCompositionMapping); + + List dataList = new ArrayList<>(); + + // Act + handler.processBefore(context, dataList); + + // Assert that updateName was called with the compositions detected + for (Map.Entry entry : expectedCompositionMapping.entrySet()) { + verify(handler).updateName(context, dataList, entry.getKey(), entry.getValue()); + } } } @Test public void testUpdateNameWithDuplicateFilenames() throws IOException { - // Arrange - List data = new ArrayList<>(); - Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); - sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(duplicateFilenames); - - // Act - handler.updateName(context, data, "composition"); - - // Assert - verify(messages, times(1)) - .error( - "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Arrange + List data = new ArrayList<>(); + Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); + when(context.getMessages()).thenReturn(messages); + + // Mock the target entity + CdsEntity targetEntity = mock(CdsEntity.class); + when(targetEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getTarget()).thenReturn(targetEntity); + + sdmUtilsMockedStatic + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "compositionName", "TestEntity")) + .thenReturn(duplicateFilenames); + + // Act + handler.updateName(context, data, "compositionDefinition", "compositionName"); + + // Assert + verify(messages, times(1)) + .error( + "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); + } } @Test @@ -128,11 +142,11 @@ public void testUpdateNameWithEmptyData() throws IOException { // Arrange List data = new ArrayList<>(); sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "compositionName", "entity")) .thenReturn(Collections.emptySet()); // Act - handler.updateName(context, data, ""); + handler.updateName(context, data, "compositionDefinition", "compositionName"); // Assert verify(messages, never()).error(anyString()); @@ -141,32 +155,37 @@ public void testUpdateNameWithEmptyData() throws IOException { @Test public void testUpdateNameWithNoAttachments() throws IOException { - // Arrange - List data = new ArrayList<>(); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); - // Create an entity map without any attachments - Map entity = new HashMap<>(); + // Arrange + List data = new ArrayList<>(); - // Wrap the entity map in CdsData - CdsData cdsDataEntity = CdsData.create(entity); + // Create an entity map without any attachments + Map entity = new HashMap<>(); - // Add the CdsData entity to the data list - data.add(cdsDataEntity); + // Wrap the entity map in CdsData + CdsData cdsDataEntity = CdsData.create(entity); - // Mock utility methods - sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(Collections.emptySet()); + // Add the CdsData entity to the data list + data.add(cdsDataEntity); - // Act - handler.updateName(context, data, ""); + // Mock utility methods + sdmUtilsMockedStatic + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "compositionName", "entity")) + .thenReturn(Collections.emptySet()); - // Assert that no updateAttachments calls were made, as there are no attachments - verify(sdmService, never()).updateAttachments(any(), any(), any(), any(), anyBoolean()); + // Act + handler.updateName(context, data, "compositionDefinition", "compositionName"); - // Assert that no error or warning messages were logged - verify(messages, never()).error(anyString()); - verify(messages, never()).warn(anyString()); + // Assert that no updateAttachments calls were made, as there are no attachments + verify(sdmService, never()).updateAttachments(any(), any(), any(), any(), anyBoolean()); + + // Assert that no error or warning messages were logged + verify(messages, never()).error(anyString()); + verify(messages, never()).warn(anyString()); + } } // @Test @@ -383,88 +402,110 @@ public void testUpdateNameWithNoAttachments() throws IOException { // } @Test public void testUpdateNameWithEmptyFilename() throws IOException { - List data = new ArrayList<>(); - Map entity = new HashMap<>(); - List> attachments = new ArrayList<>(); - - Map attachment = new HashMap<>(); - attachment.put("ID", "test-id"); - attachment.put("fileName", null); // Empty filename - attachment.put("objectId", "test-object-id"); - attachments.add(attachment); - - // entity.put("attachments", attachments); - entity.put("composition", attachments); - - CdsData cdsDataEntity = CdsData.create(entity); // Wrap entity in CdsData - data.add(cdsDataEntity); // Add to data - - // Mock duplicate file name - sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(new HashSet<>()); - - // Mock attachment entity - CdsEntity attachmentDraftEntity = mock(CdsEntity.class); - when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); - when(context.getTarget()).thenReturn(attachmentDraftEntity); - when(context.getModel()).thenReturn(model); - - // Mock findEntity to return an optional containing attachmentDraftEntity - when(model.findEntity("some.qualified.Name" + "." + "composition")) - .thenReturn(Optional.of(attachmentDraftEntity)); - UserInfo userInfo = Mockito.mock(UserInfo.class); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.isSystemUser()).thenReturn(false); - // Mock authentication - when(context.getMessages()).thenReturn(messages); - when(context.getAuthenticationInfo()).thenReturn(authInfo); - when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); - when(jwtTokenInfo.getToken()).thenReturn("testJwtToken"); - - // Mock getObject - when(sdmService.getObject("test-object-id", mockCredentials, false)) - .thenReturn("fileInSDM.txt"); - - // Mock getSecondaryTypeProperties - Map secondaryTypeProperties = new HashMap<>(); - Map updatedSecondaryProperties = new HashMap<>(); - sdmUtilsMockedStatic - .when( - () -> - SDMUtils.getSecondaryTypeProperties(Optional.of(attachmentDraftEntity), attachment)) - .thenReturn(secondaryTypeProperties); - sdmUtilsMockedStatic - .when( - () -> - SDMUtils.getUpdatedSecondaryProperties( - Optional.of(attachmentDraftEntity), - attachment, - persistenceService, - secondaryTypeProperties, - updatedSecondaryProperties)) - .thenReturn(new HashMap<>()); - - // Mock restricted character - sdmUtilsMockedStatic - .when(() -> SDMUtils.isRestrictedCharactersInName("fileNameInRequest")) - .thenReturn(false); - - when(dbQuery.getAttachmentForID(attachmentDraftEntity, persistenceService, "test-id")) - .thenReturn(null); - - // When getPropertiesForID is called - when(dbQuery.getPropertiesForID( - attachmentDraftEntity, persistenceService, "test-id", secondaryTypeProperties)) - .thenReturn(updatedSecondaryProperties); - - // Act & Assert - ServiceException exception = - assertThrows( - ServiceException.class, () -> handler.updateName(context, data, "composition")); - - // Assert that the correct exception message is returned - assertEquals("Filename cannot be empty", exception.getMessage()); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + + Map attachment = new HashMap<>(); + attachment.put("ID", "test-id"); + attachment.put("fileName", null); // Empty filename + attachment.put("objectId", "test-object-id"); + attachments.add(attachment); + + // entity.put("attachments", attachments); + entity.put("composition", attachments); + + CdsData cdsDataEntity = CdsData.create(entity); // Wrap entity in CdsData + data.add(cdsDataEntity); // Add to data + + // Mock duplicate file name + sdmUtilsMockedStatic + .when( + () -> + SDMUtils.isFileNameDuplicateInDrafts( + data, "compositionName", "some.qualified.Name")) + .thenReturn(new HashSet<>()); + + // Mock AttachmentsHandlerUtils.fetchAttachments to return the attachment with null filename + try (MockedStatic attachmentsHandlerUtilsMocked = + mockStatic(AttachmentsHandlerUtils.class)) { + attachmentsHandlerUtilsMocked + .when( + () -> + AttachmentsHandlerUtils.fetchAttachments( + "some.qualified.Name", entity, "compositionName")) + .thenReturn(attachments); + + // Mock attachment entity + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + + // Mock findEntity to return an optional containing attachmentDraftEntity + when(model.findEntity("compositionDefinition")) + .thenReturn(Optional.of(attachmentDraftEntity)); + UserInfo userInfo = Mockito.mock(UserInfo.class); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.isSystemUser()).thenReturn(false); + // Mock authentication + when(context.getMessages()).thenReturn(messages); + when(context.getAuthenticationInfo()).thenReturn(authInfo); + when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); + when(jwtTokenInfo.getToken()).thenReturn("testJwtToken"); + + // Mock getObject + when(sdmService.getObject("test-object-id", mockCredentials, false)) + .thenReturn("fileInSDM.txt"); + + // Mock getSecondaryTypeProperties + Map secondaryTypeProperties = new HashMap<>(); + Map updatedSecondaryProperties = new HashMap<>(); + sdmUtilsMockedStatic + .when( + () -> + SDMUtils.getSecondaryTypeProperties( + Optional.of(attachmentDraftEntity), attachment)) + .thenReturn(secondaryTypeProperties); + sdmUtilsMockedStatic + .when( + () -> + SDMUtils.getUpdatedSecondaryProperties( + Optional.of(attachmentDraftEntity), + attachment, + persistenceService, + secondaryTypeProperties, + updatedSecondaryProperties)) + .thenReturn(new HashMap<>()); + + // Mock restricted character + sdmUtilsMockedStatic + .when(() -> SDMUtils.isRestrictedCharactersInName("fileNameInRequest")) + .thenReturn(false); + + when(dbQuery.getAttachmentForID(attachmentDraftEntity, persistenceService, "test-id")) + .thenReturn(null); + + // When getPropertiesForID is called + when(dbQuery.getPropertiesForID( + attachmentDraftEntity, persistenceService, "test-id", secondaryTypeProperties)) + .thenReturn(updatedSecondaryProperties); + + // Act & Assert + ServiceException exception = + assertThrows( + ServiceException.class, + () -> + handler.updateName(context, data, "compositionDefinition", "compositionName")); + + // Assert that the correct exception message is returned + assertEquals("Filename cannot be empty", exception.getMessage()); + } // Close AttachmentsHandlerUtils mock + } } // @Test 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 2c9cf5259..30a550235 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 @@ -9,9 +9,11 @@ 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.SDMUpdateAttachmentsHandler; +import com.sap.cds.sdm.handler.applicationservice.helper.AttachmentsHandlerUtils; import com.sap.cds.sdm.model.CmisDocument; import com.sap.cds.sdm.model.SDMCredentials; import com.sap.cds.sdm.persistence.DBQuery; @@ -26,7 +28,7 @@ import com.sap.cds.services.request.UserInfo; import java.io.IOException; import java.util.*; -import java.util.stream.Stream; +import org.ehcache.Cache; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.*; @@ -74,54 +76,69 @@ public void tearDown() { @Test public void testProcessBefore() throws IOException { - // Arrange - List expectedCompositionNames = Arrays.asList("Name1", "Name2"); - - // Simulate a stream of CdsElement instances returned from the mock target's compositions - Stream compositionsStream = Stream.of(cdsElement, cdsElement); + try (MockedStatic attachmentsHandlerUtilsMocked = + mockStatic(AttachmentsHandlerUtils.class); + MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Arrange the mock compositions scenario + Map expectedCompositionMapping = new HashMap<>(); + expectedCompositionMapping.put("Name1", "Name1"); + expectedCompositionMapping.put("Name2", "Name2"); + + // Mock context.getTarget() and context.getModel() + when(context.getTarget()).thenReturn(targetEntity); + when(targetEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getModel()).thenReturn(model); + when(model.findEntity(anyString())).thenReturn(Optional.of(targetEntity)); + + // Mock AttachmentsHandlerUtils.getAttachmentPathMapping to return the expected mapping + attachmentsHandlerUtilsMocked + .when(() -> AttachmentsHandlerUtils.getAttachmentPathMapping(any(), any(), any())) + .thenReturn(expectedCompositionMapping); + + List dataList = new ArrayList<>(); + + // Act + handler.processBefore(context, dataList); + + // Assert that updateName was called with the compositions detected + for (Map.Entry entry : expectedCompositionMapping.entrySet()) { + verify(handler).updateName(context, dataList, entry.getKey(), entry.getValue()); + } + } + } - // mock the target and model of the context - when(context.getTarget()).thenReturn(targetEntity); - when(targetEntity.compositions()).thenReturn(compositionsStream); - when(context.getModel()).thenReturn(model); + @Test + public void testRenameWithDuplicateFilenames() throws IOException { + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); - // Mock findEntity to return an optional containing attachmentDraftEntity - when(model.findEntity(anyString())).thenReturn(Optional.of(targetEntity)); + List data = new ArrayList<>(); + Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); + when(context.getMessages()).thenReturn(messages); - // Mock the elements and their associations - when(cdsElement.getType()).thenReturn(cdsAssociationType); - when(cdsAssociationType.getTargetAspect()).thenReturn(Optional.of(targetAspect)); - when(targetAspect.getQualifiedName()).thenReturn("sap.attachments.Attachments"); - when(cdsElement.getName()).thenReturn("Name1").thenReturn("Name2"); + // Mock the target entity + CdsEntity targetEntity = mock(CdsEntity.class); + when(targetEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getTarget()).thenReturn(targetEntity); - List dataList = new ArrayList<>(); + sdmUtilsMockedStatic = mockStatic(SDMUtils.class); + sdmUtilsMockedStatic + .when(() -> isFileNameDuplicateInDrafts(data, "compositionName", "TestEntity")) + .thenReturn(duplicateFilenames); - // Act - handler.processBefore(context, dataList); + handler.updateName(context, data, "compositionDefinition", "compositionName"); - // Assert that updateName was called with the compositions detected - for (String compositionName : expectedCompositionNames) { - verify(handler).updateName(context, dataList, compositionName); + verify(messages, times(1)) + .error( + "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); } } - @Test - public void testRenameWithDuplicateFilenames() throws IOException { - List data = new ArrayList<>(); - Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); - when(context.getMessages()).thenReturn(messages); - sdmUtilsMockedStatic = mockStatic(SDMUtils.class); - sdmUtilsMockedStatic - .when(() -> isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(duplicateFilenames); - - handler.updateName(context, data, "composition"); - - verify(messages, times(1)) - .error( - "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); - } - // @Test // public void testRenameWithUniqueFilenames() throws IOException { // List data = prepareMockAttachmentData("file1.txt"); @@ -212,62 +229,125 @@ public void testRenameWithDuplicateFilenames() throws IOException { @Test public void testRenameWithNoSDMRoles() throws IOException { - // Mock the data structure to simulate the attachments - List data = new ArrayList<>(); - Map entity = new HashMap<>(); - List> attachments = new ArrayList<>(); - Map attachment = spy(new HashMap<>()); - Map secondaryProperties = new HashMap<>(); - Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); - secondaryProperties.put("filename", "file1.txt"); - - CmisDocument document = new CmisDocument(); - document.setFileName("file1.txt"); - - attachment.put("fileName", "file1.txt"); - attachment.put("url", "objectId"); - attachment.put("ID", "test-id"); - attachments.add(attachment); - - entity.put("attachments", attachments); - CdsData mockCdsData = mock(CdsData.class); - when(mockCdsData.get("composition")).thenReturn(attachments); - data.add(mockCdsData); - - CdsEntity attachmentDraftEntity = mock(CdsEntity.class); - when(context.getTarget()).thenReturn(attachmentDraftEntity); - when(context.getModel()).thenReturn(model); - when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); - when(model.findEntity("some.qualified.Name.composition")) - .thenReturn(Optional.of(attachmentDraftEntity)); - when(context.getMessages()).thenReturn(messages); - UserInfo userInfo = Mockito.mock(UserInfo.class); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.isSystemUser()).thenReturn(false); - when(tokenHandler.getSDMCredentials()).thenReturn(mockCredentials); - when(dbQuery.getAttachmentForID( - any(CdsEntity.class), any(PersistenceService.class), anyString())) - .thenReturn("file123.txt"); - - when(sdmService.updateAttachments( - mockCredentials, - document, - secondaryProperties, - secondaryPropertiesWithInvalidDefinitions, - false)) - .thenReturn(403); // Forbidden + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class); + MockedStatic attachmentsMockStatic = + mockStatic(AttachmentsHandlerUtils.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Mock the data structure to simulate the attachments + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + Map attachment = spy(new HashMap<>()); + Map secondaryProperties = new HashMap<>(); + Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); + secondaryProperties.put("filename", "file1.txt"); - // Call the method - handler.updateName(context, data, "composition"); + CmisDocument document = new CmisDocument(); + document.setFileName("file1.txt"); - // Capture and assert the warning message - ArgumentCaptor warningCaptor = ArgumentCaptor.forClass(String.class); - verify(messages).warn(warningCaptor.capture()); - String warningMessage = warningCaptor.getValue(); + attachment.put("fileName", "file1.txt"); + attachment.put("url", "objectId"); + attachment.put("ID", "test-id"); + attachments.add(attachment); - String expectedMessage = - SDMConstants.noSDMRolesMessage(Collections.singletonList("file123.txt"), "update"); - assertEquals(expectedMessage, warningMessage); + entity.put("compositionName", attachments); + CdsData mockCdsData = mock(CdsData.class); + data.add(mockCdsData); + + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + when(model.findEntity("compositionDefinition")) + .thenReturn(Optional.of(attachmentDraftEntity)); + when(context.getMessages()).thenReturn(messages); + UserInfo userInfo = Mockito.mock(UserInfo.class); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.isSystemUser()).thenReturn(false); + when(tokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + when(dbQuery.getAttachmentForID( + any(CdsEntity.class), any(PersistenceService.class), anyString())) + .thenReturn("file123.txt"); + + when(sdmService.updateAttachments( + mockCredentials, + document, + secondaryProperties, + secondaryPropertiesWithInvalidDefinitions, + false)) + .thenReturn(403); // Forbidden + + // Mock AttachmentsHandlerUtils.fetchAttachments + attachmentsMockStatic + .when( + () -> + AttachmentsHandlerUtils.fetchAttachments( + anyString(), any(Map.class), eq("compositionName"))) + .thenReturn(attachments); + + // Mock SDMUtils methods + try (MockedStatic sdmUtilsMock = mockStatic(SDMUtils.class)) { + sdmUtilsMock + .when( + () -> + SDMUtils.isFileNameDuplicateInDrafts( + any(List.class), eq("compositionName"), anyString())) + .thenReturn(Collections.emptySet()); + + sdmUtilsMock + .when(() -> SDMUtils.getPropertyTitles(any(Optional.class), any(Map.class))) + .thenReturn(Collections.emptyMap()); + + sdmUtilsMock + .when( + () -> + SDMUtils.getSecondaryPropertiesWithInvalidDefinition( + any(Optional.class), any(Map.class))) + .thenReturn(Collections.emptyMap()); + + sdmUtilsMock + .when(() -> SDMUtils.getSecondaryTypeProperties(any(Optional.class), any(Map.class))) + .thenReturn(Collections.emptyMap()); + + sdmUtilsMock + .when( + () -> + SDMUtils.getUpdatedSecondaryProperties( + any(Optional.class), + any(Map.class), + any(PersistenceService.class), + any(Map.class), + any(Map.class))) + .thenReturn(secondaryProperties); + + sdmUtilsMock + .when(() -> SDMUtils.isRestrictedCharactersInName(anyString())) + .thenReturn(false); + + // Call the method + handler.updateName(context, data, "compositionDefinition", "compositionName"); + + // Capture and assert the warning message + ArgumentCaptor warningCaptor = ArgumentCaptor.forClass(String.class); + verify(messages).warn(warningCaptor.capture()); + String warningMessage = warningCaptor.getValue(); + + String expectedMessage = + SDMConstants.noSDMRolesMessage(Collections.singletonList("file123.txt"), "update"); + assertEquals(expectedMessage, warningMessage); + } + + // Capture and assert the warning message + ArgumentCaptor warningCaptor = ArgumentCaptor.forClass(String.class); + verify(messages).warn(warningCaptor.capture()); + String warningMessage = warningCaptor.getValue(); + + String expectedMessage = + SDMConstants.noSDMRolesMessage(Collections.singletonList("file123.txt"), "update"); + assertEquals(expectedMessage, warningMessage); + } } // @Test @@ -409,35 +489,41 @@ public void testRenameWithoutFileInSDM() throws IOException { @Test public void testRenameWithNoAttachments() throws IOException { - // Arrange - List data = new ArrayList<>(); - CdsEntity attachmentDraftEntity = mock(CdsEntity.class); - Map secondaryProperties = new HashMap<>(); - Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); - CmisDocument document = new CmisDocument(); - when(context.getTarget()).thenReturn(attachmentDraftEntity); - when(context.getModel()).thenReturn(model); - - when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); - - String expectedEntityName = "some.qualified.Name.attachments"; - when(model.findEntity(expectedEntityName)).thenReturn(Optional.of(attachmentDraftEntity)); - - CdsData mockCdsData = mock(CdsData.class); - when(mockCdsData.get("attachments")).thenReturn(null); - data.add(mockCdsData); - - // Act - handler.updateName(context, data, "attachments"); - - // Assert - verify(sdmService, never()) - .updateAttachments( - eq(mockCredentials), - eq(document), - eq(secondaryProperties), - eq(secondaryPropertiesWithInvalidDefinitions), - eq(false)); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Arrange + List data = new ArrayList<>(); + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + Map secondaryProperties = new HashMap<>(); + Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); + CmisDocument document = new CmisDocument(); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + + // Mock the correct entity name that the handler will look for + when(model.findEntity("compositionDefinition")) + .thenReturn(Optional.of(attachmentDraftEntity)); + + Map entity = new HashMap<>(); + CdsData cdsDataEntity = CdsData.create(entity); + data.add(cdsDataEntity); + + // Act + handler.updateName(context, data, "compositionDefinition", "compositionName"); + + // Assert + verify(sdmService, never()) + .updateAttachments( + eq(mockCredentials), + eq(document), + eq(secondaryProperties), + eq(secondaryPropertiesWithInvalidDefinitions), + eq(false)); + } } // @Test diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java index 0a3f98001..7ef6443d5 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java @@ -157,6 +157,9 @@ void testCreate_shouldCreateLink() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -249,6 +252,9 @@ void testCreate_ShouldThrowSpecifiedExceptionWhenMaxCountReached() throws IOExce when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -307,6 +313,9 @@ void testCreate_ShouldThrowDefaultExceptionWhenMaxCountReached() throws IOExcept when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -365,6 +374,9 @@ void testCreate_ShouldThrowExceptionWhenRestrictedCharacterInLinkName() throws I when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("test/URL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -425,6 +437,9 @@ void testCreate_ThrowsServiceExceptionOnDuplicateFile() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -486,6 +501,9 @@ void testCreate_ThrowsServiceException_WhenCreateDocumentThrowsException() throw when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -554,6 +572,9 @@ void testCreate_ThrowsServiceExceptionOnDuplicateStatus() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -623,6 +644,9 @@ void testCreate_ThrowsServiceExceptionOnFailStatus() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -691,6 +715,9 @@ void testCreate_ThrowsServiceExceptionOnUnauthorizedStatus() throws IOException when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java index fa7f35387..dedb2f5ec 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java @@ -71,7 +71,6 @@ private void setUp() { @Test public void testIsFileNameDuplicateInDrafts() { List data = new ArrayList<>(); - CdsData mockCdsData = mock(CdsData.class); Map entity = new HashMap<>(); List> attachments = new ArrayList<>(); Map attachment1 = new HashMap<>(); @@ -82,11 +81,15 @@ public void testIsFileNameDuplicateInDrafts() { attachment2.put("repositoryId", "repo1"); attachments.add(attachment1); attachments.add(attachment2); - entity.put("attachments", attachments); - when(mockCdsData.get("attachments")).thenReturn(attachments); // Correctly mock get method - data.add(mockCdsData); - Set duplicateFilenames = SDMUtils.isFileNameDuplicateInDrafts(data, "attachments"); + // Create the nested structure that fetchAttachments expects + Map entityData = new HashMap<>(); + entityData.put("attachmentCompositionName", attachments); + entity.put("entity", entityData); + data.add(CdsData.create(entity)); + + Set duplicateFilenames = + SDMUtils.isFileNameDuplicateInDrafts(data, "attachmentCompositionName", "entity"); assertTrue(duplicateFilenames.contains("file1.txt")); } @@ -640,7 +643,6 @@ void testElementWithoutAnnotation() { void testElementWithAnnotation() { CdsEntity entity = mock(CdsEntity.class); CdsElement element = mock(CdsElement.class); - @SuppressWarnings("unchecked") CdsAnnotation annotation = mock(CdsAnnotation.class); when(annotation.getValue()).thenReturn("name");
This method analyzes both direct and nested attachment compositions within the given entity. + * It processes direct attachments that are immediate compositions of the entity, and also + * traverses nested compositions to find attachments in related entities. The resulting mapping + * provides a translation between logical attachment paths and their actual implementation paths. + * + * @param model the CDS model containing entity definitions and relationships + * @param entity the target CDS entity to analyze for attachment path mappings + * @param persistenceService the persistence service used for data access operations + * @return a map where keys are attachment entity paths and values are the corresponding actual + * paths, or an empty map if no attachments are found or if an error occurs during processing + */ + public static Map getAttachmentPathMapping( + CdsModel model, CdsEntity entity, PersistenceService persistenceService) { + try { + Map pathMapping = new HashMap<>(); + SDMAssociationCascader cascader = new SDMAssociationCascader(); + SDMAttachmentsReader reader = new SDMAttachmentsReader(cascader, persistenceService); + + // Process direct attachments + entity + .compositions() + .forEach( + composition -> processDirectAttachmentComposition(entity, pathMapping, composition)); + + // Process nested attachments + entity + .compositions() + .forEach( + composition -> + processNestedAttachmentComposition( + model, entity, reader, pathMapping, composition)); + + return pathMapping; + } catch (Exception e) { + logger.error(SDMConstants.FETCH_ATTACHMENT_COMPOSITION_ERROR, e.getMessage()); + return new HashMap<>(); + } + } + + private static void processDirectAttachmentComposition( + CdsEntity entity, Map pathMapping, Object composition) { + String compositionName = ((com.sap.cds.reflect.CdsElement) composition).getName(); + if (((com.sap.cds.reflect.CdsElement) composition).getType().isAssociation()) { + CdsAssociationType associationType = + (CdsAssociationType) ((com.sap.cds.reflect.CdsElement) composition).getType(); + String targetAspect = + associationType.getTargetAspect().isPresent() + ? associationType.getTargetAspect().get().getQualifiedName() + : null; + + if (isDirectAttachmentTargetAspect(targetAspect)) { + String serviceName = entity.getQualifiedName().split("\\.")[0]; + String entityName = entity.getName(); + String directPath = serviceName + "." + entityName + "." + compositionName; + pathMapping.put(directPath, directPath); + } + } + } + + private static void processNestedAttachmentComposition( + CdsModel model, + CdsEntity entity, + SDMAttachmentsReader reader, + Map pathMapping, + Object composition) { + String compositionName = ((com.sap.cds.reflect.CdsElement) composition).getName(); + String compositionTargetEntityName = ""; + + if (((com.sap.cds.reflect.CdsElement) composition).getType().isAssociation()) { + CdsAssociationType associationType = + (CdsAssociationType) ((com.sap.cds.reflect.CdsElement) composition).getType(); + String targetAspect = + associationType.getTargetAspect().isPresent() + ? associationType.getTargetAspect().get().getQualifiedName() + : null; + + if (isDirectAttachmentTargetAspect(targetAspect)) { + return; // Skip direct attachment compositions + } + + compositionTargetEntityName = associationType.getTarget().getQualifiedName(); + } + + processCompositionTargetEntity( + model, entity, reader, pathMapping, compositionName, compositionTargetEntityName); + } + + private static void processCompositionTargetEntity( + CdsModel model, + CdsEntity entity, + SDMAttachmentsReader reader, + Map pathMapping, + String compositionName, + String compositionTargetEntityName) { + if (!compositionTargetEntityName.isEmpty()) { + Optional targetEntityOpt = model.findEntity(compositionTargetEntityName); + if (targetEntityOpt.isPresent()) { + CdsEntity targetEntity = targetEntityOpt.get(); + List attachmentPaths = reader.getAttachmentEntityPaths(model, targetEntity); + processAttachmentPaths(entity, pathMapping, compositionName, targetEntity, attachmentPaths); + } + } + } + + private static void processAttachmentPaths( + CdsEntity entity, + Map pathMapping, + String compositionName, + CdsEntity targetEntity, + List attachmentPaths) { + for (String attachmentPath : attachmentPaths) { + String entityPath = buildEntityPath(entity, targetEntity, attachmentPath); + String actualPath = buildActualPath(entity, compositionName, attachmentPath); + + if (entityPath != null && actualPath != null) { + pathMapping.put(entityPath, actualPath); + } + } + } + + private static boolean isDirectAttachmentTargetAspect(String targetAspect) { + return targetAspect != null && targetAspect.equalsIgnoreCase("sap.attachments.Attachments"); + } + + /** + * Fetches attachment data from a nested entity structure based on the target entity and + * composition name. + * + * This method processes the target entity path to extract the entity name, wraps the provided + * entity data with a parent structure, and then searches for attachments within the nested + * structure. It parses the attachment composition name to identify both the attachment key (e.g., + * "attachments") and the parent key (e.g., "chapters") for precise attachment location. + * + * @param targetEntity the qualified name of the target entity (e.g., "ServiceName.EntityName") + * @param entity the entity data structure containing potential attachment information + * @param attachmentCompositionName the composition path to the attachments (e.g., + * "chapters.attachments") + * @return a list of maps representing attachment objects found in the entity structure, or an + * empty list if no attachments are found + */ + public static List> fetchAttachments( + String targetEntity, Map entity, String attachmentCompositionName) { + String[] targetEntityPath = targetEntity.split("\\."); + targetEntity = targetEntityPath[targetEntityPath.length - 1]; + entity = AttachmentsHandlerUtils.wrapEntityWithParent(entity, targetEntity.toLowerCase()); + String[] compositionParts = attachmentCompositionName.split("\\."); + String attachmentKeyFromComposition = + compositionParts[compositionParts.length - 1]; // Last part (e.g., "attachments") + String parentKeyFromComposition = + compositionParts.length >= 2 + ? compositionParts[compositionParts.length - 2].toLowerCase() + : null; // Second last part (e.g., "chapters") + + // Find all attachment arrays in the nested entity structure + return AttachmentsHandlerUtils.findNestedAttachments( + entity, attachmentKeyFromComposition, parentKeyFromComposition); + } + + private static List> findNestedAttachments( + Map entity, String attachmentKey, String parentKey) { + return findNestedAttachments(entity, attachmentKey, parentKey, null); + } + + private static String buildEntityPath( + CdsEntity parentEntity, CdsEntity targetEntity, String attachmentPath) { + try { + String[] pathParts = attachmentPath.split("\\."); + if (pathParts.length >= 3) { + // Get the service name (first part) + String serviceName = pathParts[0]; + + // Get the target entity name (without service prefix) + String targetEntityName = targetEntity.getName(); + + // Get the attachment part (last part) + String attachmentPart = pathParts[pathParts.length - 1]; + + // Build the entity path: ServiceName.EntityName.attachments + return serviceName + "." + targetEntityName + "." + attachmentPart; + } + } catch (Exception e) { + logger.warn(SDMConstants.FETCH_ATTACHMENT_COMPOSITION_ERROR, e.getMessage()); + } + return null; + } + + private static String buildActualPath( + CdsEntity parentEntity, String compositionPropertyName, String attachmentPath) { + try { + String[] pathParts = attachmentPath.split("\\."); + if (pathParts.length >= 3) { + // Get the service name (first part) + String serviceName = pathParts[0]; + + // Replace the entity name with the composition property name + // Keep the attachment part (last part) + String attachmentPart = pathParts[pathParts.length - 1]; + + // Build the new path: ServiceName.compositionPropertyName.attachments + return serviceName + "." + compositionPropertyName + "." + attachmentPart; + } + } catch (Exception e) { + logger.warn(SDMConstants.FETCH_ATTACHMENT_COMPOSITION_ERROR, e.getMessage()); + } + return null; + } + + private static List> findNestedAttachments( + Map entity, String attachmentKey, String parentKey, String currentParentKey) { + List> result = new ArrayList<>(); + + for (Map.Entry entry : entity.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + // If we found the attachment key + if (attachmentKey.equals(key) && value instanceof List) { + result.addAll(processAttachmentKey(value, key, parentKey, currentParentKey)); + } + // Recursively search in nested objects + else if (value instanceof Map) { + result.addAll(processNestedMap(value, key, attachmentKey, parentKey)); + } + // Recursively search in lists + else if (value instanceof List) { + result.addAll(processNestedList(value, key, attachmentKey, parentKey)); + } + } + + return result; + } + + private static List> processAttachmentKey( + Object value, String key, String parentKey, String currentParentKey) { + List> result = new ArrayList<>(); + + // Check if the parent matches (if parentKey is specified) + if (parentKey == null || isCorrectParentContext(currentParentKey, parentKey)) { + try { + List> attachments = (List>) value; + result.addAll(attachments); + } catch (ClassCastException e) { + logger.warn(SDMConstants.FETCH_ATTACHMENT_COMPOSITION_ERROR, e.getMessage()); + } + } + + return result; + } + + private static List> processNestedMap( + Object value, String key, String attachmentKey, String parentKey) { + List> result = new ArrayList<>(); + + try { + Map nestedMap = (Map) value; + result.addAll(findNestedAttachments(nestedMap, attachmentKey, parentKey, key)); + } catch (ClassCastException e) { + logger.warn(SDMConstants.FETCH_ATTACHMENT_COMPOSITION_ERROR, e.getMessage()); + } + + return result; + } + + private static List> processNestedList( + Object value, String key, String attachmentKey, String parentKey) { + List> result = new ArrayList<>(); + + try { + List> list = (List>) value; + for (Object item : list) { + if (item instanceof Map) { + Map itemMap = (Map) item; + result.addAll(findNestedAttachments(itemMap, attachmentKey, parentKey, key)); + } + } + } catch (ClassCastException e) { + logger.warn(SDMConstants.FETCH_ATTACHMENT_COMPOSITION_ERROR, e.getMessage()); + } + + return result; + } + + private static boolean isCorrectParentContext(String currentParentKey, String expectedParentKey) { + // If no specific parent is expected, any context is valid + if (expectedParentKey == null) { + return true; + } + + // If we're at root level (no current parent) and expecting a specific parent, no match + if (currentParentKey == null) { + return false; + } + + // Check if the current parent matches the expected parent + return expectedParentKey.equals(currentParentKey); + } + + /** + * Wraps an entity data structure with a parent container using the specified target entity name. + * + * This utility method creates a new map with the target entity name as the key and the + * provided entity data as the value. This is necessary because the root of the target entity in + * the CdsData object is not mentioned explicitly, and hence interferes with the recursive + * fetching of attachment compositions. + * + * @param root the entity data structure to be wrapped + * @param targetEntity the name to use as the parent key for wrapping the entity data + * @return a new map containing the target entity name as key and the root entity data as value + */ + public static Map wrapEntityWithParent( + Map root, String targetEntity) { + Map wrapper = new HashMap<>(); + wrapper.put(targetEntity, root); + return wrapper; + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMApplicationHandlerHelper.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMApplicationHandlerHelper.java new file mode 100644 index 000000000..6bb0824a0 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMApplicationHandlerHelper.java @@ -0,0 +1,26 @@ +package com.sap.cds.sdm.handler.common; + +import com.sap.cds.reflect.CdsStructuredType; + +/** + * The class {@link SDMApplicationHandlerHelper} provides helper methods for the SDM attachment + * application handlers. + */ +public final class SDMApplicationHandlerHelper { + private static final String ANNOTATION_IS_MEDIA_DATA = "_is_media_data"; + + /** + * Checks if the entity is a media entity. A media entity is an entity that is annotated with the + * annotation "_is_media_data". + * + * @param baseEntity The entity to check + * @return true if the entity is a media entity, false otherwise + */ + public static boolean isMediaEntity(CdsStructuredType baseEntity) { + return baseEntity.getAnnotationValue(ANNOTATION_IS_MEDIA_DATA, false); + } + + private SDMApplicationHandlerHelper() { + // avoid instantiation + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationCascader.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationCascader.java new file mode 100644 index 000000000..48e8c2734 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationCascader.java @@ -0,0 +1,97 @@ +package com.sap.cds.sdm.handler.common; + +import com.sap.cds.reflect.CdsAssociationType; +import com.sap.cds.reflect.CdsElementDefinition; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.reflect.CdsModel; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +/** + * The class {@link SDMAssociationCascader} is used to find entity paths to all media resource + * entities for a given data model. The path information is returned in a node tree which starts + * from the given entity. Only composition associations are considered. + */ +public class SDMAssociationCascader { + + public SDMNodeTree findEntityPath(CdsModel model, CdsEntity entity) { + var firstList = new LinkedList(); + var internalResultList = + getAttachmentAssociationPath( + model, entity, "", firstList, new ArrayList<>(List.of(entity.getQualifiedName()))); + + var rootTree = new SDMNodeTree(new SDMAssociationIdentifier("", entity.getQualifiedName())); + internalResultList.forEach(rootTree::addPath); + return rootTree; + } + + private List> getAttachmentAssociationPath( + CdsModel model, + CdsEntity entity, + String associationName, + LinkedList firstList, + List processedEntities) { + var internalResultList = new ArrayList>(); + var currentList = new AtomicReference>(); + var localProcessEntities = new ArrayList(); + currentList.set(new LinkedList<>()); + + var isMediaEntity = SDMApplicationHandlerHelper.isMediaEntity(entity); + if (isMediaEntity) { + var identifier = new SDMAssociationIdentifier(associationName, entity.getQualifiedName()); + firstList.addLast(identifier); + } + + if (isMediaEntity) { + internalResultList.add(firstList); + return internalResultList; + } + + Map associations = + entity + .elements() + .filter( + element -> + element.getType().isAssociation() + && element.getType().as(CdsAssociationType.class).isComposition()) + .collect( + Collectors.toMap( + CdsElementDefinition::getName, + element -> element.getType().as(CdsAssociationType.class).getTarget())); + + if (associations.isEmpty()) { + return internalResultList; + } + + var newListNeeded = false; + for (var associatedElement : associations.entrySet()) { + if (!processedEntities.contains(associatedElement.getValue().getQualifiedName())) { + if (newListNeeded) { + currentList.set(new LinkedList<>()); + currentList.get().addAll(firstList); + processedEntities = localProcessEntities; + } else { + firstList.add(new SDMAssociationIdentifier(associationName, entity.getQualifiedName())); + currentList.get().addAll(firstList); + localProcessEntities = new ArrayList<>(processedEntities); + } + processedEntities.add(associatedElement.getValue().getQualifiedName()); + newListNeeded = true; + var result = + getAttachmentAssociationPath( + model, + associatedElement.getValue(), + associatedElement.getKey(), + currentList.get(), + processedEntities); + internalResultList.addAll(result); + } + } + + return internalResultList; + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationIdentifier.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationIdentifier.java new file mode 100644 index 000000000..7ee462949 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationIdentifier.java @@ -0,0 +1,10 @@ +package com.sap.cds.sdm.handler.common; + +/** + * This record is a simple data class that holds the association name and the full entity name for + * SDM attachment processing. + * + * @param associationName the association name + * @param fullEntityName the full entity name + */ +record SDMAssociationIdentifier(String associationName, String fullEntityName) {} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAttachmentsReader.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAttachmentsReader.java new file mode 100644 index 000000000..1164cdf23 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAttachmentsReader.java @@ -0,0 +1,113 @@ +package com.sap.cds.sdm.handler.common; + +import static java.util.Objects.requireNonNull; + +import com.sap.cds.Result; +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments; +import com.sap.cds.ql.CQL; +import com.sap.cds.ql.Expand; +import com.sap.cds.ql.Select; +import com.sap.cds.ql.StructuredType; +import com.sap.cds.ql.cqn.CqnFilterableStatement; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.reflect.CdsModel; +import com.sap.cds.services.persistence.PersistenceService; +import java.util.ArrayList; +import java.util.List; + +/** + * The class {@link SDMAttachmentsReader} is used to deep read attachments from the database for a + * determined path from the given entity to the media entity. The class uses the {@link + * SDMAssociationCascader} to find the entity path. + * + * The returned data is deep including the path structure to the media entity. + */ +public class SDMAttachmentsReader { + + private final SDMAssociationCascader cascader; + private final PersistenceService persistence; + + public SDMAttachmentsReader(SDMAssociationCascader cascader, PersistenceService persistence) { + this.cascader = requireNonNull(cascader, "cascader must not be null"); + this.persistence = requireNonNull(persistence, "persistence must not be null"); + } + + public List readAttachments( + CdsModel model, CdsEntity entity, CqnFilterableStatement statement) { + + SDMNodeTree nodePath = cascader.findEntityPath(model, entity); + List> expandList = buildExpandList(nodePath); + + Select> select; + if (!expandList.isEmpty()) { + select = Select.from(statement.ref()).columns(expandList); + } else { + select = Select.from(statement.ref()).columns(StructuredType::_all); + } + + if (statement.where().isPresent()) { + select.where(statement.where().get()); + } + + Result result = persistence.run(select); + return result.listOf(Attachments.class); + } + + public List getAttachmentEntityPaths(CdsModel model, CdsEntity entity) { + SDMNodeTree nodePath = cascader.findEntityPath(model, entity); + + List attachmentPaths = new ArrayList<>(); + + if (nodePath != null) { + collectAttachmentPaths(nodePath, attachmentPaths, model); + } + return attachmentPaths; + } + + private void collectAttachmentPaths( + SDMNodeTree node, List attachmentPaths, CdsModel model) { + String entityName = node.getIdentifier().fullEntityName(); + + // Check if this entity is an attachment entity + if (isAttachmentEntity(model, entityName)) { + attachmentPaths.add(entityName); + } + + // Recursively check children + for (SDMNodeTree child : node.getChildren()) { + collectAttachmentPaths(child, attachmentPaths, model); + } + } + + private boolean isAttachmentEntity(CdsModel model, String entityName) { + var entityOpt = model.findEntity(entityName); + if (!entityOpt.isPresent()) { + return false; + } + + CdsEntity entity = entityOpt.get(); + // Check if this entity has the @_is_media_data annotation (indicating attachment entity) + return entity.getAnnotationValue("_is_media_data", false); + } + + private List> buildExpandList(SDMNodeTree root) { + List> expandResultList = new ArrayList<>(); + root.getChildren() + .forEach( + child -> { + Expand> expand = buildExpandFromTree(child); + expandResultList.add(expand); + }); + + return expandResultList; + } + + private Expand> buildExpandFromTree(SDMNodeTree node) { + if (node.getChildren().isEmpty()) { + return CQL.to(node.getIdentifier().associationName()).expand(); + } else { + return CQL.to(node.getIdentifier().associationName()) + .expand(node.getChildren().stream().map(this::buildExpandFromTree).toList()); + } + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMNodeTree.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMNodeTree.java new file mode 100644 index 000000000..71611bef2 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMNodeTree.java @@ -0,0 +1,65 @@ +package com.sap.cds.sdm.handler.common; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * The class {@link SDMNodeTree} is a tree data structure that holds the SDM association identifier + * and its children for attachment processing. + */ +class SDMNodeTree { + + private final SDMAssociationIdentifier identifier; + private final List children = new ArrayList<>(); + + SDMNodeTree(SDMAssociationIdentifier identifier) { + this.identifier = identifier; + } + + void addPath(List path) { + var currentIdentifierOptional = + path.stream() + .filter(entry -> entry.fullEntityName().equals(identifier.fullEntityName())) + .findAny(); + if (currentIdentifierOptional.isEmpty()) { + return; + } + var currentNode = this; + var index = path.indexOf(currentIdentifierOptional.get()); + if (index == path.size() - 1) { + return; + } + for (var i = index + 1; i < path.size(); i++) { + var pathEntry = path.get(i); + currentNode = currentNode.getChildOrNew(pathEntry); + } + } + + private SDMNodeTree getChildOrNew(SDMAssociationIdentifier identifier) { + var childOptional = + children.stream() + .filter(child -> child.identifier.fullEntityName().equals(identifier.fullEntityName())) + .findAny(); + if (childOptional.isPresent()) { + return childOptional.get(); + } else { + SDMNodeTree child = new SDMNodeTree(identifier); + children.add(child); + return child; + } + } + + SDMAssociationIdentifier getIdentifier() { + return identifier; + } + + List getChildren() { + return Collections.unmodifiableList(children); + } + + @Override + public String toString() { + return "SDMNodeTree{" + "identifier=" + identifier + ", children=" + children + '}'; + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java index c5c95242c..1098a1eea 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java @@ -188,9 +188,9 @@ private String getUpIdKey(CdsEntity attachmentDraftEntity) { if (upAssociation.isPresent()) { CdsElement association = upAssociation.get(); // get association type - CdsAssociationType assocType = association.getType(); + CdsAssociationType associationType = association.getType(); // get the refs of the association - List fkElements = assocType.refs().map(ref -> "up__" + ref.path()).toList(); + List fkElements = associationType.refs().map(ref -> "up__" + ref.path()).toList(); upIdKey = fkElements.get(0); } return upIdKey; diff --git a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java index b959d24e3..4cedef71b 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java @@ -1,5 +1,7 @@ package com.sap.cds.sdm.service.handler; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.sap.cds.Result; import com.sap.cds.feature.attachments.service.AttachmentService; import com.sap.cds.ql.Insert; @@ -136,9 +138,7 @@ private void createLink(EventContext context) throws IOException { String upIdKey = attachmentDraftEntity.isPresent() ? getUpIdKey(attachmentDraftEntity.get()) : "up__ID"; CqnSelect select = (CqnSelect) context.get("cqn"); - CqnAnalyzer cqnAnalyzer = CqnAnalyzer.create(cdsModel); - String id = upIdKey.replaceFirst("^up__", ""); - String upID = cqnAnalyzer.analyze(select).rootKeys().get(id).toString(); + String upID = fetchUPIDFromCQN(select); String filenameInRequest = context.get("name").toString(); Result result = @@ -214,9 +214,9 @@ private String getUpIdKey(CdsEntity attachmentDraftEntity) { if (upAssociation.isPresent()) { CdsElement association = upAssociation.get(); // get association type - CdsAssociationType assocType = association.getType(); + CdsAssociationType associationType = association.getType(); // get the refs of the association - List fkElements = assocType.refs().map(ref -> "up__" + ref.path()).toList(); + List fkElements = associationType.refs().map(ref -> "up__" + ref.path()).toList(); upIdKey = fkElements.get(0); } return upIdKey; @@ -308,14 +308,45 @@ private void handleCreateLinkResult( + ":" + context.getTarget()); - var insert = Insert.into(context.getTarget().getQualifiedName()).entry(updatedFields); - for (DraftService draftS : draftService) { - // Process each draftService object - if (context.getTarget().getQualifiedName().contains(draftS.getName())) { - draftS.newDraft(insert); + try { + var insert = Insert.into(context.getTarget().getQualifiedName()).entry(updatedFields); + for (DraftService draftS : draftService) { + if (context.getTarget().getQualifiedName().contains(draftS.getName())) { + draftS.newDraft(insert); + } } + } catch (Exception e) { + logger.info("Exception in insert : " + e.getMessage()); } context.setCompleted(); } } + + private String fetchUPIDFromCQN(CqnSelect select) { + try { + String upID = null; + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = mapper.readTree(select.toString()); + JsonNode refArray = root.path("SELECT").path("from").path("ref"); + JsonNode secondLast = refArray.get(refArray.size() - 2); + JsonNode whereArray = secondLast.path("where"); + for (int i = 0; i < whereArray.size(); i++) { + JsonNode node = whereArray.get(i); + if (node.has("ref") + && node.get("ref").isArray() + && node.get("ref").get(0).asText().equals("ID")) { + JsonNode valNode = whereArray.get(i + 2); + upID = valNode.path("val").asText(); + break; + } + } + if (upID == null) { + throw new ServiceException(SDMConstants.ENTITY_PROCESSING_ERROR_LINK); + } + return upID; + } catch (Exception e) { + logger.error(SDMConstants.ENTITY_PROCESSING_ERROR_LINK, e); + throw new ServiceException(SDMConstants.ENTITY_PROCESSING_ERROR_LINK, e); + } + } } diff --git a/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java b/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java index bf073caba..c6896b2de 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java +++ b/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java @@ -6,6 +6,7 @@ import com.sap.cds.reflect.CdsEntity; import com.sap.cds.sdm.caching.CacheConfig; import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.sdm.handler.applicationservice.helper.AttachmentsHandlerUtils; import com.sap.cds.sdm.model.AttachmentInfo; import com.sap.cds.services.persistence.PersistenceService; import java.io.IOException; @@ -32,11 +33,13 @@ private SDMUtils() { // Doesn't do anything } - public static Set isFileNameDuplicateInDrafts(List data, String composition) { + public static Set isFileNameDuplicateInDrafts( + List data, String composition, String targetEntity) { Set uniqueFilenames = new HashSet<>(); Set duplicateFilenames = new HashSet<>(); for (Map entity : data) { - List> attachments = (List>) entity.get(composition); + List> attachments = + AttachmentsHandlerUtils.fetchAttachments(targetEntity, entity, composition); if (attachments != null) { Iterator> iterator = attachments.iterator(); while (iterator.hasNext()) { diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java b/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java index 13d568e67..5a35fd04a 100644 --- a/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java @@ -530,7 +530,7 @@ public String renameAttachment( .build(); try (Response renameResponse = httpClient.newCall(request).execute()) { - if (renameResponse.code() != 200) { + if (!renameResponse.isSuccessful()) { System.out.println( "Rename Attachment failed in the " + facetName diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java b/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java index e961dc944..744b13183 100644 --- a/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java @@ -504,7 +504,7 @@ public String renameAttachment( .build(); try (Response renameResponse = httpClient.newCall(request).execute()) { - if (renameResponse.code() != 200) { + if (!renameResponse.isSuccessful()) { throw new IOException("Attachment was not renamed in section: " + facetName); } return "Renamed"; diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java b/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java index 0fcc83c38..3f97a8b7e 100644 --- a/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java @@ -604,8 +604,8 @@ void testRenameEntitiesWithUnsupportedCharacter() { counter = -1; // Reset counter for the next check response = api.saveEntityDraft(appUrl, entityName, srvpath, entityID); String expected = - "[{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 sample/1234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," - + "{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 reference1/234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," + "[{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 reference1/234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," + + "{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 sample/1234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," + "{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 footnote1/234\\n\\nRename the files and try again.\",\"numericSeverity\":3}]"; if (response.equals(expected)) { testStatus = true; @@ -673,7 +673,7 @@ void testRenameSingleDuplicate() { + "{\"code\":\"\",\"message\":\"The file(s) %s have been added multiple times. Please rename and try again.\",\"@Common.numericSeverity\":4}," + "{\"code\":\"\",\"message\":\"The file(s) %s have been added multiple times. Please rename and try again.\",\"@Common.numericSeverity\":4}" + "]}}", - name[0], name[1], name[2]); + name[1], name[0], name[2]); if (response.equals(expected)) { for (int i = 0; i < facet.length; i++) { // Attempt to rename again with a different name @@ -757,7 +757,7 @@ void testRenameEntitiesWithoutSDMRole() throws IOException { + // "\\n" + // - "\\t\\u2022 sample123\\n" + "\\t\\u2022 reference123\\n" + // "\\n" + // @@ -765,7 +765,7 @@ void testRenameEntitiesWithoutSDMRole() throws IOException { + // "\\n" + // - "\\t\\u2022 reference123\\n" + "\\t\\u2022 sample123\\n" + // "\\n" + // 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 03db40a13..dd0b4711f 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 @@ -9,8 +9,10 @@ import com.sap.cds.CdsData; import com.sap.cds.reflect.*; +import com.sap.cds.sdm.caching.CacheConfig; import com.sap.cds.sdm.handler.TokenHandler; import com.sap.cds.sdm.handler.applicationservice.SDMCreateAttachmentsHandler; +import com.sap.cds.sdm.handler.applicationservice.helper.AttachmentsHandlerUtils; import com.sap.cds.sdm.model.SDMCredentials; import com.sap.cds.sdm.persistence.DBQuery; import com.sap.cds.sdm.service.SDMService; @@ -24,7 +26,7 @@ import com.sap.cds.services.request.UserInfo; import java.io.IOException; import java.util.*; -import java.util.stream.Stream; +import org.ehcache.Cache; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -81,46 +83,58 @@ public void tearDown() { @Test public void testProcessBefore() throws IOException { - // Arrange the mock compositions scenario - List expectedCompositionNames = Arrays.asList("Name1", "Name2"); - - // Create a Stream of mocked CdsElement instances - Stream compositionsStream = Stream.of(cdsElement, cdsElement); - - when(context.getTarget().compositions()).thenReturn(compositionsStream); - when(cdsElement.getType()).thenReturn(cdsAssociationType); - when(cdsAssociationType.getTargetAspect()).thenReturn(Optional.of(targetAspect)); - when(cdsAssociationType.getTargetAspect().get().getQualifiedName()) - .thenReturn("sap.attachments.Attachments"); - when(cdsElement.getName()).thenReturn("Name1").thenReturn("Name2"); - - List dataList = new ArrayList<>(); - - // Act - handler.processBefore(context, dataList); - - // Assert that updateName was called with the compositions detected - for (String compositionName : expectedCompositionNames) { - verify(handler).updateName(context, dataList, compositionName); + try (MockedStatic attachmentsHandlerUtilsMocked = + mockStatic(AttachmentsHandlerUtils.class)) { + // Arrange the mock compositions scenario + Map expectedCompositionMapping = new HashMap<>(); + expectedCompositionMapping.put("Name1", "Name1"); + expectedCompositionMapping.put("Name2", "Name2"); + + // Mock AttachmentsHandlerUtils.getAttachmentPathMapping to return the expected mapping + attachmentsHandlerUtilsMocked + .when(() -> AttachmentsHandlerUtils.getAttachmentPathMapping(any(), any(), any())) + .thenReturn(expectedCompositionMapping); + + List dataList = new ArrayList<>(); + + // Act + handler.processBefore(context, dataList); + + // Assert that updateName was called with the compositions detected + for (Map.Entry entry : expectedCompositionMapping.entrySet()) { + verify(handler).updateName(context, dataList, entry.getKey(), entry.getValue()); + } } } @Test public void testUpdateNameWithDuplicateFilenames() throws IOException { - // Arrange - List data = new ArrayList<>(); - Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); - sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(duplicateFilenames); - - // Act - handler.updateName(context, data, "composition"); - - // Assert - verify(messages, times(1)) - .error( - "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Arrange + List data = new ArrayList<>(); + Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); + when(context.getMessages()).thenReturn(messages); + + // Mock the target entity + CdsEntity targetEntity = mock(CdsEntity.class); + when(targetEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getTarget()).thenReturn(targetEntity); + + sdmUtilsMockedStatic + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "compositionName", "TestEntity")) + .thenReturn(duplicateFilenames); + + // Act + handler.updateName(context, data, "compositionDefinition", "compositionName"); + + // Assert + verify(messages, times(1)) + .error( + "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); + } } @Test @@ -128,11 +142,11 @@ public void testUpdateNameWithEmptyData() throws IOException { // Arrange List data = new ArrayList<>(); sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "compositionName", "entity")) .thenReturn(Collections.emptySet()); // Act - handler.updateName(context, data, ""); + handler.updateName(context, data, "compositionDefinition", "compositionName"); // Assert verify(messages, never()).error(anyString()); @@ -141,32 +155,37 @@ public void testUpdateNameWithEmptyData() throws IOException { @Test public void testUpdateNameWithNoAttachments() throws IOException { - // Arrange - List data = new ArrayList<>(); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); - // Create an entity map without any attachments - Map entity = new HashMap<>(); + // Arrange + List data = new ArrayList<>(); - // Wrap the entity map in CdsData - CdsData cdsDataEntity = CdsData.create(entity); + // Create an entity map without any attachments + Map entity = new HashMap<>(); - // Add the CdsData entity to the data list - data.add(cdsDataEntity); + // Wrap the entity map in CdsData + CdsData cdsDataEntity = CdsData.create(entity); - // Mock utility methods - sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(Collections.emptySet()); + // Add the CdsData entity to the data list + data.add(cdsDataEntity); - // Act - handler.updateName(context, data, ""); + // Mock utility methods + sdmUtilsMockedStatic + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "compositionName", "entity")) + .thenReturn(Collections.emptySet()); - // Assert that no updateAttachments calls were made, as there are no attachments - verify(sdmService, never()).updateAttachments(any(), any(), any(), any(), anyBoolean()); + // Act + handler.updateName(context, data, "compositionDefinition", "compositionName"); - // Assert that no error or warning messages were logged - verify(messages, never()).error(anyString()); - verify(messages, never()).warn(anyString()); + // Assert that no updateAttachments calls were made, as there are no attachments + verify(sdmService, never()).updateAttachments(any(), any(), any(), any(), anyBoolean()); + + // Assert that no error or warning messages were logged + verify(messages, never()).error(anyString()); + verify(messages, never()).warn(anyString()); + } } // @Test @@ -383,88 +402,110 @@ public void testUpdateNameWithNoAttachments() throws IOException { // } @Test public void testUpdateNameWithEmptyFilename() throws IOException { - List data = new ArrayList<>(); - Map entity = new HashMap<>(); - List> attachments = new ArrayList<>(); - - Map attachment = new HashMap<>(); - attachment.put("ID", "test-id"); - attachment.put("fileName", null); // Empty filename - attachment.put("objectId", "test-object-id"); - attachments.add(attachment); - - // entity.put("attachments", attachments); - entity.put("composition", attachments); - - CdsData cdsDataEntity = CdsData.create(entity); // Wrap entity in CdsData - data.add(cdsDataEntity); // Add to data - - // Mock duplicate file name - sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(new HashSet<>()); - - // Mock attachment entity - CdsEntity attachmentDraftEntity = mock(CdsEntity.class); - when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); - when(context.getTarget()).thenReturn(attachmentDraftEntity); - when(context.getModel()).thenReturn(model); - - // Mock findEntity to return an optional containing attachmentDraftEntity - when(model.findEntity("some.qualified.Name" + "." + "composition")) - .thenReturn(Optional.of(attachmentDraftEntity)); - UserInfo userInfo = Mockito.mock(UserInfo.class); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.isSystemUser()).thenReturn(false); - // Mock authentication - when(context.getMessages()).thenReturn(messages); - when(context.getAuthenticationInfo()).thenReturn(authInfo); - when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); - when(jwtTokenInfo.getToken()).thenReturn("testJwtToken"); - - // Mock getObject - when(sdmService.getObject("test-object-id", mockCredentials, false)) - .thenReturn("fileInSDM.txt"); - - // Mock getSecondaryTypeProperties - Map secondaryTypeProperties = new HashMap<>(); - Map updatedSecondaryProperties = new HashMap<>(); - sdmUtilsMockedStatic - .when( - () -> - SDMUtils.getSecondaryTypeProperties(Optional.of(attachmentDraftEntity), attachment)) - .thenReturn(secondaryTypeProperties); - sdmUtilsMockedStatic - .when( - () -> - SDMUtils.getUpdatedSecondaryProperties( - Optional.of(attachmentDraftEntity), - attachment, - persistenceService, - secondaryTypeProperties, - updatedSecondaryProperties)) - .thenReturn(new HashMap<>()); - - // Mock restricted character - sdmUtilsMockedStatic - .when(() -> SDMUtils.isRestrictedCharactersInName("fileNameInRequest")) - .thenReturn(false); - - when(dbQuery.getAttachmentForID(attachmentDraftEntity, persistenceService, "test-id")) - .thenReturn(null); - - // When getPropertiesForID is called - when(dbQuery.getPropertiesForID( - attachmentDraftEntity, persistenceService, "test-id", secondaryTypeProperties)) - .thenReturn(updatedSecondaryProperties); - - // Act & Assert - ServiceException exception = - assertThrows( - ServiceException.class, () -> handler.updateName(context, data, "composition")); - - // Assert that the correct exception message is returned - assertEquals("Filename cannot be empty", exception.getMessage()); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + + Map attachment = new HashMap<>(); + attachment.put("ID", "test-id"); + attachment.put("fileName", null); // Empty filename + attachment.put("objectId", "test-object-id"); + attachments.add(attachment); + + // entity.put("attachments", attachments); + entity.put("composition", attachments); + + CdsData cdsDataEntity = CdsData.create(entity); // Wrap entity in CdsData + data.add(cdsDataEntity); // Add to data + + // Mock duplicate file name + sdmUtilsMockedStatic + .when( + () -> + SDMUtils.isFileNameDuplicateInDrafts( + data, "compositionName", "some.qualified.Name")) + .thenReturn(new HashSet<>()); + + // Mock AttachmentsHandlerUtils.fetchAttachments to return the attachment with null filename + try (MockedStatic attachmentsHandlerUtilsMocked = + mockStatic(AttachmentsHandlerUtils.class)) { + attachmentsHandlerUtilsMocked + .when( + () -> + AttachmentsHandlerUtils.fetchAttachments( + "some.qualified.Name", entity, "compositionName")) + .thenReturn(attachments); + + // Mock attachment entity + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + + // Mock findEntity to return an optional containing attachmentDraftEntity + when(model.findEntity("compositionDefinition")) + .thenReturn(Optional.of(attachmentDraftEntity)); + UserInfo userInfo = Mockito.mock(UserInfo.class); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.isSystemUser()).thenReturn(false); + // Mock authentication + when(context.getMessages()).thenReturn(messages); + when(context.getAuthenticationInfo()).thenReturn(authInfo); + when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); + when(jwtTokenInfo.getToken()).thenReturn("testJwtToken"); + + // Mock getObject + when(sdmService.getObject("test-object-id", mockCredentials, false)) + .thenReturn("fileInSDM.txt"); + + // Mock getSecondaryTypeProperties + Map secondaryTypeProperties = new HashMap<>(); + Map updatedSecondaryProperties = new HashMap<>(); + sdmUtilsMockedStatic + .when( + () -> + SDMUtils.getSecondaryTypeProperties( + Optional.of(attachmentDraftEntity), attachment)) + .thenReturn(secondaryTypeProperties); + sdmUtilsMockedStatic + .when( + () -> + SDMUtils.getUpdatedSecondaryProperties( + Optional.of(attachmentDraftEntity), + attachment, + persistenceService, + secondaryTypeProperties, + updatedSecondaryProperties)) + .thenReturn(new HashMap<>()); + + // Mock restricted character + sdmUtilsMockedStatic + .when(() -> SDMUtils.isRestrictedCharactersInName("fileNameInRequest")) + .thenReturn(false); + + when(dbQuery.getAttachmentForID(attachmentDraftEntity, persistenceService, "test-id")) + .thenReturn(null); + + // When getPropertiesForID is called + when(dbQuery.getPropertiesForID( + attachmentDraftEntity, persistenceService, "test-id", secondaryTypeProperties)) + .thenReturn(updatedSecondaryProperties); + + // Act & Assert + ServiceException exception = + assertThrows( + ServiceException.class, + () -> + handler.updateName(context, data, "compositionDefinition", "compositionName")); + + // Assert that the correct exception message is returned + assertEquals("Filename cannot be empty", exception.getMessage()); + } // Close AttachmentsHandlerUtils mock + } } // @Test 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 2c9cf5259..30a550235 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 @@ -9,9 +9,11 @@ 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.SDMUpdateAttachmentsHandler; +import com.sap.cds.sdm.handler.applicationservice.helper.AttachmentsHandlerUtils; import com.sap.cds.sdm.model.CmisDocument; import com.sap.cds.sdm.model.SDMCredentials; import com.sap.cds.sdm.persistence.DBQuery; @@ -26,7 +28,7 @@ import com.sap.cds.services.request.UserInfo; import java.io.IOException; import java.util.*; -import java.util.stream.Stream; +import org.ehcache.Cache; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.*; @@ -74,54 +76,69 @@ public void tearDown() { @Test public void testProcessBefore() throws IOException { - // Arrange - List expectedCompositionNames = Arrays.asList("Name1", "Name2"); - - // Simulate a stream of CdsElement instances returned from the mock target's compositions - Stream compositionsStream = Stream.of(cdsElement, cdsElement); + try (MockedStatic attachmentsHandlerUtilsMocked = + mockStatic(AttachmentsHandlerUtils.class); + MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Arrange the mock compositions scenario + Map expectedCompositionMapping = new HashMap<>(); + expectedCompositionMapping.put("Name1", "Name1"); + expectedCompositionMapping.put("Name2", "Name2"); + + // Mock context.getTarget() and context.getModel() + when(context.getTarget()).thenReturn(targetEntity); + when(targetEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getModel()).thenReturn(model); + when(model.findEntity(anyString())).thenReturn(Optional.of(targetEntity)); + + // Mock AttachmentsHandlerUtils.getAttachmentPathMapping to return the expected mapping + attachmentsHandlerUtilsMocked + .when(() -> AttachmentsHandlerUtils.getAttachmentPathMapping(any(), any(), any())) + .thenReturn(expectedCompositionMapping); + + List dataList = new ArrayList<>(); + + // Act + handler.processBefore(context, dataList); + + // Assert that updateName was called with the compositions detected + for (Map.Entry entry : expectedCompositionMapping.entrySet()) { + verify(handler).updateName(context, dataList, entry.getKey(), entry.getValue()); + } + } + } - // mock the target and model of the context - when(context.getTarget()).thenReturn(targetEntity); - when(targetEntity.compositions()).thenReturn(compositionsStream); - when(context.getModel()).thenReturn(model); + @Test + public void testRenameWithDuplicateFilenames() throws IOException { + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); - // Mock findEntity to return an optional containing attachmentDraftEntity - when(model.findEntity(anyString())).thenReturn(Optional.of(targetEntity)); + List data = new ArrayList<>(); + Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); + when(context.getMessages()).thenReturn(messages); - // Mock the elements and their associations - when(cdsElement.getType()).thenReturn(cdsAssociationType); - when(cdsAssociationType.getTargetAspect()).thenReturn(Optional.of(targetAspect)); - when(targetAspect.getQualifiedName()).thenReturn("sap.attachments.Attachments"); - when(cdsElement.getName()).thenReturn("Name1").thenReturn("Name2"); + // Mock the target entity + CdsEntity targetEntity = mock(CdsEntity.class); + when(targetEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getTarget()).thenReturn(targetEntity); - List dataList = new ArrayList<>(); + sdmUtilsMockedStatic = mockStatic(SDMUtils.class); + sdmUtilsMockedStatic + .when(() -> isFileNameDuplicateInDrafts(data, "compositionName", "TestEntity")) + .thenReturn(duplicateFilenames); - // Act - handler.processBefore(context, dataList); + handler.updateName(context, data, "compositionDefinition", "compositionName"); - // Assert that updateName was called with the compositions detected - for (String compositionName : expectedCompositionNames) { - verify(handler).updateName(context, dataList, compositionName); + verify(messages, times(1)) + .error( + "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); } } - @Test - public void testRenameWithDuplicateFilenames() throws IOException { - List data = new ArrayList<>(); - Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); - when(context.getMessages()).thenReturn(messages); - sdmUtilsMockedStatic = mockStatic(SDMUtils.class); - sdmUtilsMockedStatic - .when(() -> isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(duplicateFilenames); - - handler.updateName(context, data, "composition"); - - verify(messages, times(1)) - .error( - "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); - } - // @Test // public void testRenameWithUniqueFilenames() throws IOException { // List data = prepareMockAttachmentData("file1.txt"); @@ -212,62 +229,125 @@ public void testRenameWithDuplicateFilenames() throws IOException { @Test public void testRenameWithNoSDMRoles() throws IOException { - // Mock the data structure to simulate the attachments - List data = new ArrayList<>(); - Map entity = new HashMap<>(); - List> attachments = new ArrayList<>(); - Map attachment = spy(new HashMap<>()); - Map secondaryProperties = new HashMap<>(); - Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); - secondaryProperties.put("filename", "file1.txt"); - - CmisDocument document = new CmisDocument(); - document.setFileName("file1.txt"); - - attachment.put("fileName", "file1.txt"); - attachment.put("url", "objectId"); - attachment.put("ID", "test-id"); - attachments.add(attachment); - - entity.put("attachments", attachments); - CdsData mockCdsData = mock(CdsData.class); - when(mockCdsData.get("composition")).thenReturn(attachments); - data.add(mockCdsData); - - CdsEntity attachmentDraftEntity = mock(CdsEntity.class); - when(context.getTarget()).thenReturn(attachmentDraftEntity); - when(context.getModel()).thenReturn(model); - when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); - when(model.findEntity("some.qualified.Name.composition")) - .thenReturn(Optional.of(attachmentDraftEntity)); - when(context.getMessages()).thenReturn(messages); - UserInfo userInfo = Mockito.mock(UserInfo.class); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.isSystemUser()).thenReturn(false); - when(tokenHandler.getSDMCredentials()).thenReturn(mockCredentials); - when(dbQuery.getAttachmentForID( - any(CdsEntity.class), any(PersistenceService.class), anyString())) - .thenReturn("file123.txt"); - - when(sdmService.updateAttachments( - mockCredentials, - document, - secondaryProperties, - secondaryPropertiesWithInvalidDefinitions, - false)) - .thenReturn(403); // Forbidden + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class); + MockedStatic attachmentsMockStatic = + mockStatic(AttachmentsHandlerUtils.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Mock the data structure to simulate the attachments + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + Map attachment = spy(new HashMap<>()); + Map secondaryProperties = new HashMap<>(); + Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); + secondaryProperties.put("filename", "file1.txt"); - // Call the method - handler.updateName(context, data, "composition"); + CmisDocument document = new CmisDocument(); + document.setFileName("file1.txt"); - // Capture and assert the warning message - ArgumentCaptor warningCaptor = ArgumentCaptor.forClass(String.class); - verify(messages).warn(warningCaptor.capture()); - String warningMessage = warningCaptor.getValue(); + attachment.put("fileName", "file1.txt"); + attachment.put("url", "objectId"); + attachment.put("ID", "test-id"); + attachments.add(attachment); - String expectedMessage = - SDMConstants.noSDMRolesMessage(Collections.singletonList("file123.txt"), "update"); - assertEquals(expectedMessage, warningMessage); + entity.put("compositionName", attachments); + CdsData mockCdsData = mock(CdsData.class); + data.add(mockCdsData); + + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + when(model.findEntity("compositionDefinition")) + .thenReturn(Optional.of(attachmentDraftEntity)); + when(context.getMessages()).thenReturn(messages); + UserInfo userInfo = Mockito.mock(UserInfo.class); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.isSystemUser()).thenReturn(false); + when(tokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + when(dbQuery.getAttachmentForID( + any(CdsEntity.class), any(PersistenceService.class), anyString())) + .thenReturn("file123.txt"); + + when(sdmService.updateAttachments( + mockCredentials, + document, + secondaryProperties, + secondaryPropertiesWithInvalidDefinitions, + false)) + .thenReturn(403); // Forbidden + + // Mock AttachmentsHandlerUtils.fetchAttachments + attachmentsMockStatic + .when( + () -> + AttachmentsHandlerUtils.fetchAttachments( + anyString(), any(Map.class), eq("compositionName"))) + .thenReturn(attachments); + + // Mock SDMUtils methods + try (MockedStatic sdmUtilsMock = mockStatic(SDMUtils.class)) { + sdmUtilsMock + .when( + () -> + SDMUtils.isFileNameDuplicateInDrafts( + any(List.class), eq("compositionName"), anyString())) + .thenReturn(Collections.emptySet()); + + sdmUtilsMock + .when(() -> SDMUtils.getPropertyTitles(any(Optional.class), any(Map.class))) + .thenReturn(Collections.emptyMap()); + + sdmUtilsMock + .when( + () -> + SDMUtils.getSecondaryPropertiesWithInvalidDefinition( + any(Optional.class), any(Map.class))) + .thenReturn(Collections.emptyMap()); + + sdmUtilsMock + .when(() -> SDMUtils.getSecondaryTypeProperties(any(Optional.class), any(Map.class))) + .thenReturn(Collections.emptyMap()); + + sdmUtilsMock + .when( + () -> + SDMUtils.getUpdatedSecondaryProperties( + any(Optional.class), + any(Map.class), + any(PersistenceService.class), + any(Map.class), + any(Map.class))) + .thenReturn(secondaryProperties); + + sdmUtilsMock + .when(() -> SDMUtils.isRestrictedCharactersInName(anyString())) + .thenReturn(false); + + // Call the method + handler.updateName(context, data, "compositionDefinition", "compositionName"); + + // Capture and assert the warning message + ArgumentCaptor warningCaptor = ArgumentCaptor.forClass(String.class); + verify(messages).warn(warningCaptor.capture()); + String warningMessage = warningCaptor.getValue(); + + String expectedMessage = + SDMConstants.noSDMRolesMessage(Collections.singletonList("file123.txt"), "update"); + assertEquals(expectedMessage, warningMessage); + } + + // Capture and assert the warning message + ArgumentCaptor warningCaptor = ArgumentCaptor.forClass(String.class); + verify(messages).warn(warningCaptor.capture()); + String warningMessage = warningCaptor.getValue(); + + String expectedMessage = + SDMConstants.noSDMRolesMessage(Collections.singletonList("file123.txt"), "update"); + assertEquals(expectedMessage, warningMessage); + } } // @Test @@ -409,35 +489,41 @@ public void testRenameWithoutFileInSDM() throws IOException { @Test public void testRenameWithNoAttachments() throws IOException { - // Arrange - List data = new ArrayList<>(); - CdsEntity attachmentDraftEntity = mock(CdsEntity.class); - Map secondaryProperties = new HashMap<>(); - Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); - CmisDocument document = new CmisDocument(); - when(context.getTarget()).thenReturn(attachmentDraftEntity); - when(context.getModel()).thenReturn(model); - - when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); - - String expectedEntityName = "some.qualified.Name.attachments"; - when(model.findEntity(expectedEntityName)).thenReturn(Optional.of(attachmentDraftEntity)); - - CdsData mockCdsData = mock(CdsData.class); - when(mockCdsData.get("attachments")).thenReturn(null); - data.add(mockCdsData); - - // Act - handler.updateName(context, data, "attachments"); - - // Assert - verify(sdmService, never()) - .updateAttachments( - eq(mockCredentials), - eq(document), - eq(secondaryProperties), - eq(secondaryPropertiesWithInvalidDefinitions), - eq(false)); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Arrange + List data = new ArrayList<>(); + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + Map secondaryProperties = new HashMap<>(); + Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); + CmisDocument document = new CmisDocument(); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + + // Mock the correct entity name that the handler will look for + when(model.findEntity("compositionDefinition")) + .thenReturn(Optional.of(attachmentDraftEntity)); + + Map entity = new HashMap<>(); + CdsData cdsDataEntity = CdsData.create(entity); + data.add(cdsDataEntity); + + // Act + handler.updateName(context, data, "compositionDefinition", "compositionName"); + + // Assert + verify(sdmService, never()) + .updateAttachments( + eq(mockCredentials), + eq(document), + eq(secondaryProperties), + eq(secondaryPropertiesWithInvalidDefinitions), + eq(false)); + } } // @Test diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java index 0a3f98001..7ef6443d5 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java @@ -157,6 +157,9 @@ void testCreate_shouldCreateLink() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -249,6 +252,9 @@ void testCreate_ShouldThrowSpecifiedExceptionWhenMaxCountReached() throws IOExce when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -307,6 +313,9 @@ void testCreate_ShouldThrowDefaultExceptionWhenMaxCountReached() throws IOExcept when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -365,6 +374,9 @@ void testCreate_ShouldThrowExceptionWhenRestrictedCharacterInLinkName() throws I when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("test/URL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -425,6 +437,9 @@ void testCreate_ThrowsServiceExceptionOnDuplicateFile() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -486,6 +501,9 @@ void testCreate_ThrowsServiceException_WhenCreateDocumentThrowsException() throw when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -554,6 +572,9 @@ void testCreate_ThrowsServiceExceptionOnDuplicateStatus() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -623,6 +644,9 @@ void testCreate_ThrowsServiceExceptionOnFailStatus() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -691,6 +715,9 @@ void testCreate_ThrowsServiceExceptionOnUnauthorizedStatus() throws IOException when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java index fa7f35387..dedb2f5ec 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java @@ -71,7 +71,6 @@ private void setUp() { @Test public void testIsFileNameDuplicateInDrafts() { List data = new ArrayList<>(); - CdsData mockCdsData = mock(CdsData.class); Map entity = new HashMap<>(); List> attachments = new ArrayList<>(); Map attachment1 = new HashMap<>(); @@ -82,11 +81,15 @@ public void testIsFileNameDuplicateInDrafts() { attachment2.put("repositoryId", "repo1"); attachments.add(attachment1); attachments.add(attachment2); - entity.put("attachments", attachments); - when(mockCdsData.get("attachments")).thenReturn(attachments); // Correctly mock get method - data.add(mockCdsData); - Set duplicateFilenames = SDMUtils.isFileNameDuplicateInDrafts(data, "attachments"); + // Create the nested structure that fetchAttachments expects + Map entityData = new HashMap<>(); + entityData.put("attachmentCompositionName", attachments); + entity.put("entity", entityData); + data.add(CdsData.create(entity)); + + Set duplicateFilenames = + SDMUtils.isFileNameDuplicateInDrafts(data, "attachmentCompositionName", "entity"); assertTrue(duplicateFilenames.contains("file1.txt")); } @@ -640,7 +643,6 @@ void testElementWithoutAnnotation() { void testElementWithAnnotation() { CdsEntity entity = mock(CdsEntity.class); CdsElement element = mock(CdsElement.class); - @SuppressWarnings("unchecked") CdsAnnotation annotation = mock(CdsAnnotation.class); when(annotation.getValue()).thenReturn("name");
This method processes the target entity path to extract the entity name, wraps the provided + * entity data with a parent structure, and then searches for attachments within the nested + * structure. It parses the attachment composition name to identify both the attachment key (e.g., + * "attachments") and the parent key (e.g., "chapters") for precise attachment location. + * + * @param targetEntity the qualified name of the target entity (e.g., "ServiceName.EntityName") + * @param entity the entity data structure containing potential attachment information + * @param attachmentCompositionName the composition path to the attachments (e.g., + * "chapters.attachments") + * @return a list of maps representing attachment objects found in the entity structure, or an + * empty list if no attachments are found + */ + public static List> fetchAttachments( + String targetEntity, Map entity, String attachmentCompositionName) { + String[] targetEntityPath = targetEntity.split("\\."); + targetEntity = targetEntityPath[targetEntityPath.length - 1]; + entity = AttachmentsHandlerUtils.wrapEntityWithParent(entity, targetEntity.toLowerCase()); + String[] compositionParts = attachmentCompositionName.split("\\."); + String attachmentKeyFromComposition = + compositionParts[compositionParts.length - 1]; // Last part (e.g., "attachments") + String parentKeyFromComposition = + compositionParts.length >= 2 + ? compositionParts[compositionParts.length - 2].toLowerCase() + : null; // Second last part (e.g., "chapters") + + // Find all attachment arrays in the nested entity structure + return AttachmentsHandlerUtils.findNestedAttachments( + entity, attachmentKeyFromComposition, parentKeyFromComposition); + } + + private static List> findNestedAttachments( + Map entity, String attachmentKey, String parentKey) { + return findNestedAttachments(entity, attachmentKey, parentKey, null); + } + + private static String buildEntityPath( + CdsEntity parentEntity, CdsEntity targetEntity, String attachmentPath) { + try { + String[] pathParts = attachmentPath.split("\\."); + if (pathParts.length >= 3) { + // Get the service name (first part) + String serviceName = pathParts[0]; + + // Get the target entity name (without service prefix) + String targetEntityName = targetEntity.getName(); + + // Get the attachment part (last part) + String attachmentPart = pathParts[pathParts.length - 1]; + + // Build the entity path: ServiceName.EntityName.attachments + return serviceName + "." + targetEntityName + "." + attachmentPart; + } + } catch (Exception e) { + logger.warn(SDMConstants.FETCH_ATTACHMENT_COMPOSITION_ERROR, e.getMessage()); + } + return null; + } + + private static String buildActualPath( + CdsEntity parentEntity, String compositionPropertyName, String attachmentPath) { + try { + String[] pathParts = attachmentPath.split("\\."); + if (pathParts.length >= 3) { + // Get the service name (first part) + String serviceName = pathParts[0]; + + // Replace the entity name with the composition property name + // Keep the attachment part (last part) + String attachmentPart = pathParts[pathParts.length - 1]; + + // Build the new path: ServiceName.compositionPropertyName.attachments + return serviceName + "." + compositionPropertyName + "." + attachmentPart; + } + } catch (Exception e) { + logger.warn(SDMConstants.FETCH_ATTACHMENT_COMPOSITION_ERROR, e.getMessage()); + } + return null; + } + + private static List> findNestedAttachments( + Map entity, String attachmentKey, String parentKey, String currentParentKey) { + List> result = new ArrayList<>(); + + for (Map.Entry entry : entity.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + // If we found the attachment key + if (attachmentKey.equals(key) && value instanceof List) { + result.addAll(processAttachmentKey(value, key, parentKey, currentParentKey)); + } + // Recursively search in nested objects + else if (value instanceof Map) { + result.addAll(processNestedMap(value, key, attachmentKey, parentKey)); + } + // Recursively search in lists + else if (value instanceof List) { + result.addAll(processNestedList(value, key, attachmentKey, parentKey)); + } + } + + return result; + } + + private static List> processAttachmentKey( + Object value, String key, String parentKey, String currentParentKey) { + List> result = new ArrayList<>(); + + // Check if the parent matches (if parentKey is specified) + if (parentKey == null || isCorrectParentContext(currentParentKey, parentKey)) { + try { + List> attachments = (List>) value; + result.addAll(attachments); + } catch (ClassCastException e) { + logger.warn(SDMConstants.FETCH_ATTACHMENT_COMPOSITION_ERROR, e.getMessage()); + } + } + + return result; + } + + private static List> processNestedMap( + Object value, String key, String attachmentKey, String parentKey) { + List> result = new ArrayList<>(); + + try { + Map nestedMap = (Map) value; + result.addAll(findNestedAttachments(nestedMap, attachmentKey, parentKey, key)); + } catch (ClassCastException e) { + logger.warn(SDMConstants.FETCH_ATTACHMENT_COMPOSITION_ERROR, e.getMessage()); + } + + return result; + } + + private static List> processNestedList( + Object value, String key, String attachmentKey, String parentKey) { + List> result = new ArrayList<>(); + + try { + List> list = (List>) value; + for (Object item : list) { + if (item instanceof Map) { + Map itemMap = (Map) item; + result.addAll(findNestedAttachments(itemMap, attachmentKey, parentKey, key)); + } + } + } catch (ClassCastException e) { + logger.warn(SDMConstants.FETCH_ATTACHMENT_COMPOSITION_ERROR, e.getMessage()); + } + + return result; + } + + private static boolean isCorrectParentContext(String currentParentKey, String expectedParentKey) { + // If no specific parent is expected, any context is valid + if (expectedParentKey == null) { + return true; + } + + // If we're at root level (no current parent) and expecting a specific parent, no match + if (currentParentKey == null) { + return false; + } + + // Check if the current parent matches the expected parent + return expectedParentKey.equals(currentParentKey); + } + + /** + * Wraps an entity data structure with a parent container using the specified target entity name. + * + * This utility method creates a new map with the target entity name as the key and the + * provided entity data as the value. This is necessary because the root of the target entity in + * the CdsData object is not mentioned explicitly, and hence interferes with the recursive + * fetching of attachment compositions. + * + * @param root the entity data structure to be wrapped + * @param targetEntity the name to use as the parent key for wrapping the entity data + * @return a new map containing the target entity name as key and the root entity data as value + */ + public static Map wrapEntityWithParent( + Map root, String targetEntity) { + Map wrapper = new HashMap<>(); + wrapper.put(targetEntity, root); + return wrapper; + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMApplicationHandlerHelper.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMApplicationHandlerHelper.java new file mode 100644 index 000000000..6bb0824a0 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMApplicationHandlerHelper.java @@ -0,0 +1,26 @@ +package com.sap.cds.sdm.handler.common; + +import com.sap.cds.reflect.CdsStructuredType; + +/** + * The class {@link SDMApplicationHandlerHelper} provides helper methods for the SDM attachment + * application handlers. + */ +public final class SDMApplicationHandlerHelper { + private static final String ANNOTATION_IS_MEDIA_DATA = "_is_media_data"; + + /** + * Checks if the entity is a media entity. A media entity is an entity that is annotated with the + * annotation "_is_media_data". + * + * @param baseEntity The entity to check + * @return true if the entity is a media entity, false otherwise + */ + public static boolean isMediaEntity(CdsStructuredType baseEntity) { + return baseEntity.getAnnotationValue(ANNOTATION_IS_MEDIA_DATA, false); + } + + private SDMApplicationHandlerHelper() { + // avoid instantiation + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationCascader.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationCascader.java new file mode 100644 index 000000000..48e8c2734 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationCascader.java @@ -0,0 +1,97 @@ +package com.sap.cds.sdm.handler.common; + +import com.sap.cds.reflect.CdsAssociationType; +import com.sap.cds.reflect.CdsElementDefinition; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.reflect.CdsModel; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +/** + * The class {@link SDMAssociationCascader} is used to find entity paths to all media resource + * entities for a given data model. The path information is returned in a node tree which starts + * from the given entity. Only composition associations are considered. + */ +public class SDMAssociationCascader { + + public SDMNodeTree findEntityPath(CdsModel model, CdsEntity entity) { + var firstList = new LinkedList(); + var internalResultList = + getAttachmentAssociationPath( + model, entity, "", firstList, new ArrayList<>(List.of(entity.getQualifiedName()))); + + var rootTree = new SDMNodeTree(new SDMAssociationIdentifier("", entity.getQualifiedName())); + internalResultList.forEach(rootTree::addPath); + return rootTree; + } + + private List> getAttachmentAssociationPath( + CdsModel model, + CdsEntity entity, + String associationName, + LinkedList firstList, + List processedEntities) { + var internalResultList = new ArrayList>(); + var currentList = new AtomicReference>(); + var localProcessEntities = new ArrayList(); + currentList.set(new LinkedList<>()); + + var isMediaEntity = SDMApplicationHandlerHelper.isMediaEntity(entity); + if (isMediaEntity) { + var identifier = new SDMAssociationIdentifier(associationName, entity.getQualifiedName()); + firstList.addLast(identifier); + } + + if (isMediaEntity) { + internalResultList.add(firstList); + return internalResultList; + } + + Map associations = + entity + .elements() + .filter( + element -> + element.getType().isAssociation() + && element.getType().as(CdsAssociationType.class).isComposition()) + .collect( + Collectors.toMap( + CdsElementDefinition::getName, + element -> element.getType().as(CdsAssociationType.class).getTarget())); + + if (associations.isEmpty()) { + return internalResultList; + } + + var newListNeeded = false; + for (var associatedElement : associations.entrySet()) { + if (!processedEntities.contains(associatedElement.getValue().getQualifiedName())) { + if (newListNeeded) { + currentList.set(new LinkedList<>()); + currentList.get().addAll(firstList); + processedEntities = localProcessEntities; + } else { + firstList.add(new SDMAssociationIdentifier(associationName, entity.getQualifiedName())); + currentList.get().addAll(firstList); + localProcessEntities = new ArrayList<>(processedEntities); + } + processedEntities.add(associatedElement.getValue().getQualifiedName()); + newListNeeded = true; + var result = + getAttachmentAssociationPath( + model, + associatedElement.getValue(), + associatedElement.getKey(), + currentList.get(), + processedEntities); + internalResultList.addAll(result); + } + } + + return internalResultList; + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationIdentifier.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationIdentifier.java new file mode 100644 index 000000000..7ee462949 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationIdentifier.java @@ -0,0 +1,10 @@ +package com.sap.cds.sdm.handler.common; + +/** + * This record is a simple data class that holds the association name and the full entity name for + * SDM attachment processing. + * + * @param associationName the association name + * @param fullEntityName the full entity name + */ +record SDMAssociationIdentifier(String associationName, String fullEntityName) {} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAttachmentsReader.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAttachmentsReader.java new file mode 100644 index 000000000..1164cdf23 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAttachmentsReader.java @@ -0,0 +1,113 @@ +package com.sap.cds.sdm.handler.common; + +import static java.util.Objects.requireNonNull; + +import com.sap.cds.Result; +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments; +import com.sap.cds.ql.CQL; +import com.sap.cds.ql.Expand; +import com.sap.cds.ql.Select; +import com.sap.cds.ql.StructuredType; +import com.sap.cds.ql.cqn.CqnFilterableStatement; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.reflect.CdsModel; +import com.sap.cds.services.persistence.PersistenceService; +import java.util.ArrayList; +import java.util.List; + +/** + * The class {@link SDMAttachmentsReader} is used to deep read attachments from the database for a + * determined path from the given entity to the media entity. The class uses the {@link + * SDMAssociationCascader} to find the entity path. + * + * The returned data is deep including the path structure to the media entity. + */ +public class SDMAttachmentsReader { + + private final SDMAssociationCascader cascader; + private final PersistenceService persistence; + + public SDMAttachmentsReader(SDMAssociationCascader cascader, PersistenceService persistence) { + this.cascader = requireNonNull(cascader, "cascader must not be null"); + this.persistence = requireNonNull(persistence, "persistence must not be null"); + } + + public List readAttachments( + CdsModel model, CdsEntity entity, CqnFilterableStatement statement) { + + SDMNodeTree nodePath = cascader.findEntityPath(model, entity); + List> expandList = buildExpandList(nodePath); + + Select> select; + if (!expandList.isEmpty()) { + select = Select.from(statement.ref()).columns(expandList); + } else { + select = Select.from(statement.ref()).columns(StructuredType::_all); + } + + if (statement.where().isPresent()) { + select.where(statement.where().get()); + } + + Result result = persistence.run(select); + return result.listOf(Attachments.class); + } + + public List getAttachmentEntityPaths(CdsModel model, CdsEntity entity) { + SDMNodeTree nodePath = cascader.findEntityPath(model, entity); + + List attachmentPaths = new ArrayList<>(); + + if (nodePath != null) { + collectAttachmentPaths(nodePath, attachmentPaths, model); + } + return attachmentPaths; + } + + private void collectAttachmentPaths( + SDMNodeTree node, List attachmentPaths, CdsModel model) { + String entityName = node.getIdentifier().fullEntityName(); + + // Check if this entity is an attachment entity + if (isAttachmentEntity(model, entityName)) { + attachmentPaths.add(entityName); + } + + // Recursively check children + for (SDMNodeTree child : node.getChildren()) { + collectAttachmentPaths(child, attachmentPaths, model); + } + } + + private boolean isAttachmentEntity(CdsModel model, String entityName) { + var entityOpt = model.findEntity(entityName); + if (!entityOpt.isPresent()) { + return false; + } + + CdsEntity entity = entityOpt.get(); + // Check if this entity has the @_is_media_data annotation (indicating attachment entity) + return entity.getAnnotationValue("_is_media_data", false); + } + + private List> buildExpandList(SDMNodeTree root) { + List> expandResultList = new ArrayList<>(); + root.getChildren() + .forEach( + child -> { + Expand> expand = buildExpandFromTree(child); + expandResultList.add(expand); + }); + + return expandResultList; + } + + private Expand> buildExpandFromTree(SDMNodeTree node) { + if (node.getChildren().isEmpty()) { + return CQL.to(node.getIdentifier().associationName()).expand(); + } else { + return CQL.to(node.getIdentifier().associationName()) + .expand(node.getChildren().stream().map(this::buildExpandFromTree).toList()); + } + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMNodeTree.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMNodeTree.java new file mode 100644 index 000000000..71611bef2 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMNodeTree.java @@ -0,0 +1,65 @@ +package com.sap.cds.sdm.handler.common; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * The class {@link SDMNodeTree} is a tree data structure that holds the SDM association identifier + * and its children for attachment processing. + */ +class SDMNodeTree { + + private final SDMAssociationIdentifier identifier; + private final List children = new ArrayList<>(); + + SDMNodeTree(SDMAssociationIdentifier identifier) { + this.identifier = identifier; + } + + void addPath(List path) { + var currentIdentifierOptional = + path.stream() + .filter(entry -> entry.fullEntityName().equals(identifier.fullEntityName())) + .findAny(); + if (currentIdentifierOptional.isEmpty()) { + return; + } + var currentNode = this; + var index = path.indexOf(currentIdentifierOptional.get()); + if (index == path.size() - 1) { + return; + } + for (var i = index + 1; i < path.size(); i++) { + var pathEntry = path.get(i); + currentNode = currentNode.getChildOrNew(pathEntry); + } + } + + private SDMNodeTree getChildOrNew(SDMAssociationIdentifier identifier) { + var childOptional = + children.stream() + .filter(child -> child.identifier.fullEntityName().equals(identifier.fullEntityName())) + .findAny(); + if (childOptional.isPresent()) { + return childOptional.get(); + } else { + SDMNodeTree child = new SDMNodeTree(identifier); + children.add(child); + return child; + } + } + + SDMAssociationIdentifier getIdentifier() { + return identifier; + } + + List getChildren() { + return Collections.unmodifiableList(children); + } + + @Override + public String toString() { + return "SDMNodeTree{" + "identifier=" + identifier + ", children=" + children + '}'; + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java index c5c95242c..1098a1eea 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java @@ -188,9 +188,9 @@ private String getUpIdKey(CdsEntity attachmentDraftEntity) { if (upAssociation.isPresent()) { CdsElement association = upAssociation.get(); // get association type - CdsAssociationType assocType = association.getType(); + CdsAssociationType associationType = association.getType(); // get the refs of the association - List fkElements = assocType.refs().map(ref -> "up__" + ref.path()).toList(); + List fkElements = associationType.refs().map(ref -> "up__" + ref.path()).toList(); upIdKey = fkElements.get(0); } return upIdKey; diff --git a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java index b959d24e3..4cedef71b 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java @@ -1,5 +1,7 @@ package com.sap.cds.sdm.service.handler; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.sap.cds.Result; import com.sap.cds.feature.attachments.service.AttachmentService; import com.sap.cds.ql.Insert; @@ -136,9 +138,7 @@ private void createLink(EventContext context) throws IOException { String upIdKey = attachmentDraftEntity.isPresent() ? getUpIdKey(attachmentDraftEntity.get()) : "up__ID"; CqnSelect select = (CqnSelect) context.get("cqn"); - CqnAnalyzer cqnAnalyzer = CqnAnalyzer.create(cdsModel); - String id = upIdKey.replaceFirst("^up__", ""); - String upID = cqnAnalyzer.analyze(select).rootKeys().get(id).toString(); + String upID = fetchUPIDFromCQN(select); String filenameInRequest = context.get("name").toString(); Result result = @@ -214,9 +214,9 @@ private String getUpIdKey(CdsEntity attachmentDraftEntity) { if (upAssociation.isPresent()) { CdsElement association = upAssociation.get(); // get association type - CdsAssociationType assocType = association.getType(); + CdsAssociationType associationType = association.getType(); // get the refs of the association - List fkElements = assocType.refs().map(ref -> "up__" + ref.path()).toList(); + List fkElements = associationType.refs().map(ref -> "up__" + ref.path()).toList(); upIdKey = fkElements.get(0); } return upIdKey; @@ -308,14 +308,45 @@ private void handleCreateLinkResult( + ":" + context.getTarget()); - var insert = Insert.into(context.getTarget().getQualifiedName()).entry(updatedFields); - for (DraftService draftS : draftService) { - // Process each draftService object - if (context.getTarget().getQualifiedName().contains(draftS.getName())) { - draftS.newDraft(insert); + try { + var insert = Insert.into(context.getTarget().getQualifiedName()).entry(updatedFields); + for (DraftService draftS : draftService) { + if (context.getTarget().getQualifiedName().contains(draftS.getName())) { + draftS.newDraft(insert); + } } + } catch (Exception e) { + logger.info("Exception in insert : " + e.getMessage()); } context.setCompleted(); } } + + private String fetchUPIDFromCQN(CqnSelect select) { + try { + String upID = null; + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = mapper.readTree(select.toString()); + JsonNode refArray = root.path("SELECT").path("from").path("ref"); + JsonNode secondLast = refArray.get(refArray.size() - 2); + JsonNode whereArray = secondLast.path("where"); + for (int i = 0; i < whereArray.size(); i++) { + JsonNode node = whereArray.get(i); + if (node.has("ref") + && node.get("ref").isArray() + && node.get("ref").get(0).asText().equals("ID")) { + JsonNode valNode = whereArray.get(i + 2); + upID = valNode.path("val").asText(); + break; + } + } + if (upID == null) { + throw new ServiceException(SDMConstants.ENTITY_PROCESSING_ERROR_LINK); + } + return upID; + } catch (Exception e) { + logger.error(SDMConstants.ENTITY_PROCESSING_ERROR_LINK, e); + throw new ServiceException(SDMConstants.ENTITY_PROCESSING_ERROR_LINK, e); + } + } } diff --git a/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java b/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java index bf073caba..c6896b2de 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java +++ b/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java @@ -6,6 +6,7 @@ import com.sap.cds.reflect.CdsEntity; import com.sap.cds.sdm.caching.CacheConfig; import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.sdm.handler.applicationservice.helper.AttachmentsHandlerUtils; import com.sap.cds.sdm.model.AttachmentInfo; import com.sap.cds.services.persistence.PersistenceService; import java.io.IOException; @@ -32,11 +33,13 @@ private SDMUtils() { // Doesn't do anything } - public static Set isFileNameDuplicateInDrafts(List data, String composition) { + public static Set isFileNameDuplicateInDrafts( + List data, String composition, String targetEntity) { Set uniqueFilenames = new HashSet<>(); Set duplicateFilenames = new HashSet<>(); for (Map entity : data) { - List> attachments = (List>) entity.get(composition); + List> attachments = + AttachmentsHandlerUtils.fetchAttachments(targetEntity, entity, composition); if (attachments != null) { Iterator> iterator = attachments.iterator(); while (iterator.hasNext()) { diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java b/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java index 13d568e67..5a35fd04a 100644 --- a/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java @@ -530,7 +530,7 @@ public String renameAttachment( .build(); try (Response renameResponse = httpClient.newCall(request).execute()) { - if (renameResponse.code() != 200) { + if (!renameResponse.isSuccessful()) { System.out.println( "Rename Attachment failed in the " + facetName diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java b/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java index e961dc944..744b13183 100644 --- a/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java @@ -504,7 +504,7 @@ public String renameAttachment( .build(); try (Response renameResponse = httpClient.newCall(request).execute()) { - if (renameResponse.code() != 200) { + if (!renameResponse.isSuccessful()) { throw new IOException("Attachment was not renamed in section: " + facetName); } return "Renamed"; diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java b/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java index 0fcc83c38..3f97a8b7e 100644 --- a/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java @@ -604,8 +604,8 @@ void testRenameEntitiesWithUnsupportedCharacter() { counter = -1; // Reset counter for the next check response = api.saveEntityDraft(appUrl, entityName, srvpath, entityID); String expected = - "[{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 sample/1234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," - + "{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 reference1/234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," + "[{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 reference1/234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," + + "{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 sample/1234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," + "{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 footnote1/234\\n\\nRename the files and try again.\",\"numericSeverity\":3}]"; if (response.equals(expected)) { testStatus = true; @@ -673,7 +673,7 @@ void testRenameSingleDuplicate() { + "{\"code\":\"\",\"message\":\"The file(s) %s have been added multiple times. Please rename and try again.\",\"@Common.numericSeverity\":4}," + "{\"code\":\"\",\"message\":\"The file(s) %s have been added multiple times. Please rename and try again.\",\"@Common.numericSeverity\":4}" + "]}}", - name[0], name[1], name[2]); + name[1], name[0], name[2]); if (response.equals(expected)) { for (int i = 0; i < facet.length; i++) { // Attempt to rename again with a different name @@ -757,7 +757,7 @@ void testRenameEntitiesWithoutSDMRole() throws IOException { + // "\\n" + // - "\\t\\u2022 sample123\\n" + "\\t\\u2022 reference123\\n" + // "\\n" + // @@ -765,7 +765,7 @@ void testRenameEntitiesWithoutSDMRole() throws IOException { + // "\\n" + // - "\\t\\u2022 reference123\\n" + "\\t\\u2022 sample123\\n" + // "\\n" + // 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 03db40a13..dd0b4711f 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 @@ -9,8 +9,10 @@ import com.sap.cds.CdsData; import com.sap.cds.reflect.*; +import com.sap.cds.sdm.caching.CacheConfig; import com.sap.cds.sdm.handler.TokenHandler; import com.sap.cds.sdm.handler.applicationservice.SDMCreateAttachmentsHandler; +import com.sap.cds.sdm.handler.applicationservice.helper.AttachmentsHandlerUtils; import com.sap.cds.sdm.model.SDMCredentials; import com.sap.cds.sdm.persistence.DBQuery; import com.sap.cds.sdm.service.SDMService; @@ -24,7 +26,7 @@ import com.sap.cds.services.request.UserInfo; import java.io.IOException; import java.util.*; -import java.util.stream.Stream; +import org.ehcache.Cache; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -81,46 +83,58 @@ public void tearDown() { @Test public void testProcessBefore() throws IOException { - // Arrange the mock compositions scenario - List expectedCompositionNames = Arrays.asList("Name1", "Name2"); - - // Create a Stream of mocked CdsElement instances - Stream compositionsStream = Stream.of(cdsElement, cdsElement); - - when(context.getTarget().compositions()).thenReturn(compositionsStream); - when(cdsElement.getType()).thenReturn(cdsAssociationType); - when(cdsAssociationType.getTargetAspect()).thenReturn(Optional.of(targetAspect)); - when(cdsAssociationType.getTargetAspect().get().getQualifiedName()) - .thenReturn("sap.attachments.Attachments"); - when(cdsElement.getName()).thenReturn("Name1").thenReturn("Name2"); - - List dataList = new ArrayList<>(); - - // Act - handler.processBefore(context, dataList); - - // Assert that updateName was called with the compositions detected - for (String compositionName : expectedCompositionNames) { - verify(handler).updateName(context, dataList, compositionName); + try (MockedStatic attachmentsHandlerUtilsMocked = + mockStatic(AttachmentsHandlerUtils.class)) { + // Arrange the mock compositions scenario + Map expectedCompositionMapping = new HashMap<>(); + expectedCompositionMapping.put("Name1", "Name1"); + expectedCompositionMapping.put("Name2", "Name2"); + + // Mock AttachmentsHandlerUtils.getAttachmentPathMapping to return the expected mapping + attachmentsHandlerUtilsMocked + .when(() -> AttachmentsHandlerUtils.getAttachmentPathMapping(any(), any(), any())) + .thenReturn(expectedCompositionMapping); + + List dataList = new ArrayList<>(); + + // Act + handler.processBefore(context, dataList); + + // Assert that updateName was called with the compositions detected + for (Map.Entry entry : expectedCompositionMapping.entrySet()) { + verify(handler).updateName(context, dataList, entry.getKey(), entry.getValue()); + } } } @Test public void testUpdateNameWithDuplicateFilenames() throws IOException { - // Arrange - List data = new ArrayList<>(); - Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); - sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(duplicateFilenames); - - // Act - handler.updateName(context, data, "composition"); - - // Assert - verify(messages, times(1)) - .error( - "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Arrange + List data = new ArrayList<>(); + Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); + when(context.getMessages()).thenReturn(messages); + + // Mock the target entity + CdsEntity targetEntity = mock(CdsEntity.class); + when(targetEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getTarget()).thenReturn(targetEntity); + + sdmUtilsMockedStatic + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "compositionName", "TestEntity")) + .thenReturn(duplicateFilenames); + + // Act + handler.updateName(context, data, "compositionDefinition", "compositionName"); + + // Assert + verify(messages, times(1)) + .error( + "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); + } } @Test @@ -128,11 +142,11 @@ public void testUpdateNameWithEmptyData() throws IOException { // Arrange List data = new ArrayList<>(); sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "compositionName", "entity")) .thenReturn(Collections.emptySet()); // Act - handler.updateName(context, data, ""); + handler.updateName(context, data, "compositionDefinition", "compositionName"); // Assert verify(messages, never()).error(anyString()); @@ -141,32 +155,37 @@ public void testUpdateNameWithEmptyData() throws IOException { @Test public void testUpdateNameWithNoAttachments() throws IOException { - // Arrange - List data = new ArrayList<>(); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); - // Create an entity map without any attachments - Map entity = new HashMap<>(); + // Arrange + List data = new ArrayList<>(); - // Wrap the entity map in CdsData - CdsData cdsDataEntity = CdsData.create(entity); + // Create an entity map without any attachments + Map entity = new HashMap<>(); - // Add the CdsData entity to the data list - data.add(cdsDataEntity); + // Wrap the entity map in CdsData + CdsData cdsDataEntity = CdsData.create(entity); - // Mock utility methods - sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(Collections.emptySet()); + // Add the CdsData entity to the data list + data.add(cdsDataEntity); - // Act - handler.updateName(context, data, ""); + // Mock utility methods + sdmUtilsMockedStatic + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "compositionName", "entity")) + .thenReturn(Collections.emptySet()); - // Assert that no updateAttachments calls were made, as there are no attachments - verify(sdmService, never()).updateAttachments(any(), any(), any(), any(), anyBoolean()); + // Act + handler.updateName(context, data, "compositionDefinition", "compositionName"); - // Assert that no error or warning messages were logged - verify(messages, never()).error(anyString()); - verify(messages, never()).warn(anyString()); + // Assert that no updateAttachments calls were made, as there are no attachments + verify(sdmService, never()).updateAttachments(any(), any(), any(), any(), anyBoolean()); + + // Assert that no error or warning messages were logged + verify(messages, never()).error(anyString()); + verify(messages, never()).warn(anyString()); + } } // @Test @@ -383,88 +402,110 @@ public void testUpdateNameWithNoAttachments() throws IOException { // } @Test public void testUpdateNameWithEmptyFilename() throws IOException { - List data = new ArrayList<>(); - Map entity = new HashMap<>(); - List> attachments = new ArrayList<>(); - - Map attachment = new HashMap<>(); - attachment.put("ID", "test-id"); - attachment.put("fileName", null); // Empty filename - attachment.put("objectId", "test-object-id"); - attachments.add(attachment); - - // entity.put("attachments", attachments); - entity.put("composition", attachments); - - CdsData cdsDataEntity = CdsData.create(entity); // Wrap entity in CdsData - data.add(cdsDataEntity); // Add to data - - // Mock duplicate file name - sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(new HashSet<>()); - - // Mock attachment entity - CdsEntity attachmentDraftEntity = mock(CdsEntity.class); - when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); - when(context.getTarget()).thenReturn(attachmentDraftEntity); - when(context.getModel()).thenReturn(model); - - // Mock findEntity to return an optional containing attachmentDraftEntity - when(model.findEntity("some.qualified.Name" + "." + "composition")) - .thenReturn(Optional.of(attachmentDraftEntity)); - UserInfo userInfo = Mockito.mock(UserInfo.class); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.isSystemUser()).thenReturn(false); - // Mock authentication - when(context.getMessages()).thenReturn(messages); - when(context.getAuthenticationInfo()).thenReturn(authInfo); - when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); - when(jwtTokenInfo.getToken()).thenReturn("testJwtToken"); - - // Mock getObject - when(sdmService.getObject("test-object-id", mockCredentials, false)) - .thenReturn("fileInSDM.txt"); - - // Mock getSecondaryTypeProperties - Map secondaryTypeProperties = new HashMap<>(); - Map updatedSecondaryProperties = new HashMap<>(); - sdmUtilsMockedStatic - .when( - () -> - SDMUtils.getSecondaryTypeProperties(Optional.of(attachmentDraftEntity), attachment)) - .thenReturn(secondaryTypeProperties); - sdmUtilsMockedStatic - .when( - () -> - SDMUtils.getUpdatedSecondaryProperties( - Optional.of(attachmentDraftEntity), - attachment, - persistenceService, - secondaryTypeProperties, - updatedSecondaryProperties)) - .thenReturn(new HashMap<>()); - - // Mock restricted character - sdmUtilsMockedStatic - .when(() -> SDMUtils.isRestrictedCharactersInName("fileNameInRequest")) - .thenReturn(false); - - when(dbQuery.getAttachmentForID(attachmentDraftEntity, persistenceService, "test-id")) - .thenReturn(null); - - // When getPropertiesForID is called - when(dbQuery.getPropertiesForID( - attachmentDraftEntity, persistenceService, "test-id", secondaryTypeProperties)) - .thenReturn(updatedSecondaryProperties); - - // Act & Assert - ServiceException exception = - assertThrows( - ServiceException.class, () -> handler.updateName(context, data, "composition")); - - // Assert that the correct exception message is returned - assertEquals("Filename cannot be empty", exception.getMessage()); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + + Map attachment = new HashMap<>(); + attachment.put("ID", "test-id"); + attachment.put("fileName", null); // Empty filename + attachment.put("objectId", "test-object-id"); + attachments.add(attachment); + + // entity.put("attachments", attachments); + entity.put("composition", attachments); + + CdsData cdsDataEntity = CdsData.create(entity); // Wrap entity in CdsData + data.add(cdsDataEntity); // Add to data + + // Mock duplicate file name + sdmUtilsMockedStatic + .when( + () -> + SDMUtils.isFileNameDuplicateInDrafts( + data, "compositionName", "some.qualified.Name")) + .thenReturn(new HashSet<>()); + + // Mock AttachmentsHandlerUtils.fetchAttachments to return the attachment with null filename + try (MockedStatic attachmentsHandlerUtilsMocked = + mockStatic(AttachmentsHandlerUtils.class)) { + attachmentsHandlerUtilsMocked + .when( + () -> + AttachmentsHandlerUtils.fetchAttachments( + "some.qualified.Name", entity, "compositionName")) + .thenReturn(attachments); + + // Mock attachment entity + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + + // Mock findEntity to return an optional containing attachmentDraftEntity + when(model.findEntity("compositionDefinition")) + .thenReturn(Optional.of(attachmentDraftEntity)); + UserInfo userInfo = Mockito.mock(UserInfo.class); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.isSystemUser()).thenReturn(false); + // Mock authentication + when(context.getMessages()).thenReturn(messages); + when(context.getAuthenticationInfo()).thenReturn(authInfo); + when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); + when(jwtTokenInfo.getToken()).thenReturn("testJwtToken"); + + // Mock getObject + when(sdmService.getObject("test-object-id", mockCredentials, false)) + .thenReturn("fileInSDM.txt"); + + // Mock getSecondaryTypeProperties + Map secondaryTypeProperties = new HashMap<>(); + Map updatedSecondaryProperties = new HashMap<>(); + sdmUtilsMockedStatic + .when( + () -> + SDMUtils.getSecondaryTypeProperties( + Optional.of(attachmentDraftEntity), attachment)) + .thenReturn(secondaryTypeProperties); + sdmUtilsMockedStatic + .when( + () -> + SDMUtils.getUpdatedSecondaryProperties( + Optional.of(attachmentDraftEntity), + attachment, + persistenceService, + secondaryTypeProperties, + updatedSecondaryProperties)) + .thenReturn(new HashMap<>()); + + // Mock restricted character + sdmUtilsMockedStatic + .when(() -> SDMUtils.isRestrictedCharactersInName("fileNameInRequest")) + .thenReturn(false); + + when(dbQuery.getAttachmentForID(attachmentDraftEntity, persistenceService, "test-id")) + .thenReturn(null); + + // When getPropertiesForID is called + when(dbQuery.getPropertiesForID( + attachmentDraftEntity, persistenceService, "test-id", secondaryTypeProperties)) + .thenReturn(updatedSecondaryProperties); + + // Act & Assert + ServiceException exception = + assertThrows( + ServiceException.class, + () -> + handler.updateName(context, data, "compositionDefinition", "compositionName")); + + // Assert that the correct exception message is returned + assertEquals("Filename cannot be empty", exception.getMessage()); + } // Close AttachmentsHandlerUtils mock + } } // @Test 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 2c9cf5259..30a550235 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 @@ -9,9 +9,11 @@ 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.SDMUpdateAttachmentsHandler; +import com.sap.cds.sdm.handler.applicationservice.helper.AttachmentsHandlerUtils; import com.sap.cds.sdm.model.CmisDocument; import com.sap.cds.sdm.model.SDMCredentials; import com.sap.cds.sdm.persistence.DBQuery; @@ -26,7 +28,7 @@ import com.sap.cds.services.request.UserInfo; import java.io.IOException; import java.util.*; -import java.util.stream.Stream; +import org.ehcache.Cache; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.*; @@ -74,54 +76,69 @@ public void tearDown() { @Test public void testProcessBefore() throws IOException { - // Arrange - List expectedCompositionNames = Arrays.asList("Name1", "Name2"); - - // Simulate a stream of CdsElement instances returned from the mock target's compositions - Stream compositionsStream = Stream.of(cdsElement, cdsElement); + try (MockedStatic attachmentsHandlerUtilsMocked = + mockStatic(AttachmentsHandlerUtils.class); + MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Arrange the mock compositions scenario + Map expectedCompositionMapping = new HashMap<>(); + expectedCompositionMapping.put("Name1", "Name1"); + expectedCompositionMapping.put("Name2", "Name2"); + + // Mock context.getTarget() and context.getModel() + when(context.getTarget()).thenReturn(targetEntity); + when(targetEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getModel()).thenReturn(model); + when(model.findEntity(anyString())).thenReturn(Optional.of(targetEntity)); + + // Mock AttachmentsHandlerUtils.getAttachmentPathMapping to return the expected mapping + attachmentsHandlerUtilsMocked + .when(() -> AttachmentsHandlerUtils.getAttachmentPathMapping(any(), any(), any())) + .thenReturn(expectedCompositionMapping); + + List dataList = new ArrayList<>(); + + // Act + handler.processBefore(context, dataList); + + // Assert that updateName was called with the compositions detected + for (Map.Entry entry : expectedCompositionMapping.entrySet()) { + verify(handler).updateName(context, dataList, entry.getKey(), entry.getValue()); + } + } + } - // mock the target and model of the context - when(context.getTarget()).thenReturn(targetEntity); - when(targetEntity.compositions()).thenReturn(compositionsStream); - when(context.getModel()).thenReturn(model); + @Test + public void testRenameWithDuplicateFilenames() throws IOException { + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); - // Mock findEntity to return an optional containing attachmentDraftEntity - when(model.findEntity(anyString())).thenReturn(Optional.of(targetEntity)); + List data = new ArrayList<>(); + Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); + when(context.getMessages()).thenReturn(messages); - // Mock the elements and their associations - when(cdsElement.getType()).thenReturn(cdsAssociationType); - when(cdsAssociationType.getTargetAspect()).thenReturn(Optional.of(targetAspect)); - when(targetAspect.getQualifiedName()).thenReturn("sap.attachments.Attachments"); - when(cdsElement.getName()).thenReturn("Name1").thenReturn("Name2"); + // Mock the target entity + CdsEntity targetEntity = mock(CdsEntity.class); + when(targetEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getTarget()).thenReturn(targetEntity); - List dataList = new ArrayList<>(); + sdmUtilsMockedStatic = mockStatic(SDMUtils.class); + sdmUtilsMockedStatic + .when(() -> isFileNameDuplicateInDrafts(data, "compositionName", "TestEntity")) + .thenReturn(duplicateFilenames); - // Act - handler.processBefore(context, dataList); + handler.updateName(context, data, "compositionDefinition", "compositionName"); - // Assert that updateName was called with the compositions detected - for (String compositionName : expectedCompositionNames) { - verify(handler).updateName(context, dataList, compositionName); + verify(messages, times(1)) + .error( + "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); } } - @Test - public void testRenameWithDuplicateFilenames() throws IOException { - List data = new ArrayList<>(); - Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); - when(context.getMessages()).thenReturn(messages); - sdmUtilsMockedStatic = mockStatic(SDMUtils.class); - sdmUtilsMockedStatic - .when(() -> isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(duplicateFilenames); - - handler.updateName(context, data, "composition"); - - verify(messages, times(1)) - .error( - "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); - } - // @Test // public void testRenameWithUniqueFilenames() throws IOException { // List data = prepareMockAttachmentData("file1.txt"); @@ -212,62 +229,125 @@ public void testRenameWithDuplicateFilenames() throws IOException { @Test public void testRenameWithNoSDMRoles() throws IOException { - // Mock the data structure to simulate the attachments - List data = new ArrayList<>(); - Map entity = new HashMap<>(); - List> attachments = new ArrayList<>(); - Map attachment = spy(new HashMap<>()); - Map secondaryProperties = new HashMap<>(); - Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); - secondaryProperties.put("filename", "file1.txt"); - - CmisDocument document = new CmisDocument(); - document.setFileName("file1.txt"); - - attachment.put("fileName", "file1.txt"); - attachment.put("url", "objectId"); - attachment.put("ID", "test-id"); - attachments.add(attachment); - - entity.put("attachments", attachments); - CdsData mockCdsData = mock(CdsData.class); - when(mockCdsData.get("composition")).thenReturn(attachments); - data.add(mockCdsData); - - CdsEntity attachmentDraftEntity = mock(CdsEntity.class); - when(context.getTarget()).thenReturn(attachmentDraftEntity); - when(context.getModel()).thenReturn(model); - when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); - when(model.findEntity("some.qualified.Name.composition")) - .thenReturn(Optional.of(attachmentDraftEntity)); - when(context.getMessages()).thenReturn(messages); - UserInfo userInfo = Mockito.mock(UserInfo.class); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.isSystemUser()).thenReturn(false); - when(tokenHandler.getSDMCredentials()).thenReturn(mockCredentials); - when(dbQuery.getAttachmentForID( - any(CdsEntity.class), any(PersistenceService.class), anyString())) - .thenReturn("file123.txt"); - - when(sdmService.updateAttachments( - mockCredentials, - document, - secondaryProperties, - secondaryPropertiesWithInvalidDefinitions, - false)) - .thenReturn(403); // Forbidden + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class); + MockedStatic attachmentsMockStatic = + mockStatic(AttachmentsHandlerUtils.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Mock the data structure to simulate the attachments + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + Map attachment = spy(new HashMap<>()); + Map secondaryProperties = new HashMap<>(); + Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); + secondaryProperties.put("filename", "file1.txt"); - // Call the method - handler.updateName(context, data, "composition"); + CmisDocument document = new CmisDocument(); + document.setFileName("file1.txt"); - // Capture and assert the warning message - ArgumentCaptor warningCaptor = ArgumentCaptor.forClass(String.class); - verify(messages).warn(warningCaptor.capture()); - String warningMessage = warningCaptor.getValue(); + attachment.put("fileName", "file1.txt"); + attachment.put("url", "objectId"); + attachment.put("ID", "test-id"); + attachments.add(attachment); - String expectedMessage = - SDMConstants.noSDMRolesMessage(Collections.singletonList("file123.txt"), "update"); - assertEquals(expectedMessage, warningMessage); + entity.put("compositionName", attachments); + CdsData mockCdsData = mock(CdsData.class); + data.add(mockCdsData); + + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + when(model.findEntity("compositionDefinition")) + .thenReturn(Optional.of(attachmentDraftEntity)); + when(context.getMessages()).thenReturn(messages); + UserInfo userInfo = Mockito.mock(UserInfo.class); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.isSystemUser()).thenReturn(false); + when(tokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + when(dbQuery.getAttachmentForID( + any(CdsEntity.class), any(PersistenceService.class), anyString())) + .thenReturn("file123.txt"); + + when(sdmService.updateAttachments( + mockCredentials, + document, + secondaryProperties, + secondaryPropertiesWithInvalidDefinitions, + false)) + .thenReturn(403); // Forbidden + + // Mock AttachmentsHandlerUtils.fetchAttachments + attachmentsMockStatic + .when( + () -> + AttachmentsHandlerUtils.fetchAttachments( + anyString(), any(Map.class), eq("compositionName"))) + .thenReturn(attachments); + + // Mock SDMUtils methods + try (MockedStatic sdmUtilsMock = mockStatic(SDMUtils.class)) { + sdmUtilsMock + .when( + () -> + SDMUtils.isFileNameDuplicateInDrafts( + any(List.class), eq("compositionName"), anyString())) + .thenReturn(Collections.emptySet()); + + sdmUtilsMock + .when(() -> SDMUtils.getPropertyTitles(any(Optional.class), any(Map.class))) + .thenReturn(Collections.emptyMap()); + + sdmUtilsMock + .when( + () -> + SDMUtils.getSecondaryPropertiesWithInvalidDefinition( + any(Optional.class), any(Map.class))) + .thenReturn(Collections.emptyMap()); + + sdmUtilsMock + .when(() -> SDMUtils.getSecondaryTypeProperties(any(Optional.class), any(Map.class))) + .thenReturn(Collections.emptyMap()); + + sdmUtilsMock + .when( + () -> + SDMUtils.getUpdatedSecondaryProperties( + any(Optional.class), + any(Map.class), + any(PersistenceService.class), + any(Map.class), + any(Map.class))) + .thenReturn(secondaryProperties); + + sdmUtilsMock + .when(() -> SDMUtils.isRestrictedCharactersInName(anyString())) + .thenReturn(false); + + // Call the method + handler.updateName(context, data, "compositionDefinition", "compositionName"); + + // Capture and assert the warning message + ArgumentCaptor warningCaptor = ArgumentCaptor.forClass(String.class); + verify(messages).warn(warningCaptor.capture()); + String warningMessage = warningCaptor.getValue(); + + String expectedMessage = + SDMConstants.noSDMRolesMessage(Collections.singletonList("file123.txt"), "update"); + assertEquals(expectedMessage, warningMessage); + } + + // Capture and assert the warning message + ArgumentCaptor warningCaptor = ArgumentCaptor.forClass(String.class); + verify(messages).warn(warningCaptor.capture()); + String warningMessage = warningCaptor.getValue(); + + String expectedMessage = + SDMConstants.noSDMRolesMessage(Collections.singletonList("file123.txt"), "update"); + assertEquals(expectedMessage, warningMessage); + } } // @Test @@ -409,35 +489,41 @@ public void testRenameWithoutFileInSDM() throws IOException { @Test public void testRenameWithNoAttachments() throws IOException { - // Arrange - List data = new ArrayList<>(); - CdsEntity attachmentDraftEntity = mock(CdsEntity.class); - Map secondaryProperties = new HashMap<>(); - Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); - CmisDocument document = new CmisDocument(); - when(context.getTarget()).thenReturn(attachmentDraftEntity); - when(context.getModel()).thenReturn(model); - - when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); - - String expectedEntityName = "some.qualified.Name.attachments"; - when(model.findEntity(expectedEntityName)).thenReturn(Optional.of(attachmentDraftEntity)); - - CdsData mockCdsData = mock(CdsData.class); - when(mockCdsData.get("attachments")).thenReturn(null); - data.add(mockCdsData); - - // Act - handler.updateName(context, data, "attachments"); - - // Assert - verify(sdmService, never()) - .updateAttachments( - eq(mockCredentials), - eq(document), - eq(secondaryProperties), - eq(secondaryPropertiesWithInvalidDefinitions), - eq(false)); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Arrange + List data = new ArrayList<>(); + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + Map secondaryProperties = new HashMap<>(); + Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); + CmisDocument document = new CmisDocument(); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + + // Mock the correct entity name that the handler will look for + when(model.findEntity("compositionDefinition")) + .thenReturn(Optional.of(attachmentDraftEntity)); + + Map entity = new HashMap<>(); + CdsData cdsDataEntity = CdsData.create(entity); + data.add(cdsDataEntity); + + // Act + handler.updateName(context, data, "compositionDefinition", "compositionName"); + + // Assert + verify(sdmService, never()) + .updateAttachments( + eq(mockCredentials), + eq(document), + eq(secondaryProperties), + eq(secondaryPropertiesWithInvalidDefinitions), + eq(false)); + } } // @Test diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java index 0a3f98001..7ef6443d5 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java @@ -157,6 +157,9 @@ void testCreate_shouldCreateLink() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -249,6 +252,9 @@ void testCreate_ShouldThrowSpecifiedExceptionWhenMaxCountReached() throws IOExce when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -307,6 +313,9 @@ void testCreate_ShouldThrowDefaultExceptionWhenMaxCountReached() throws IOExcept when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -365,6 +374,9 @@ void testCreate_ShouldThrowExceptionWhenRestrictedCharacterInLinkName() throws I when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("test/URL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -425,6 +437,9 @@ void testCreate_ThrowsServiceExceptionOnDuplicateFile() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -486,6 +501,9 @@ void testCreate_ThrowsServiceException_WhenCreateDocumentThrowsException() throw when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -554,6 +572,9 @@ void testCreate_ThrowsServiceExceptionOnDuplicateStatus() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -623,6 +644,9 @@ void testCreate_ThrowsServiceExceptionOnFailStatus() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -691,6 +715,9 @@ void testCreate_ThrowsServiceExceptionOnUnauthorizedStatus() throws IOException when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java index fa7f35387..dedb2f5ec 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java @@ -71,7 +71,6 @@ private void setUp() { @Test public void testIsFileNameDuplicateInDrafts() { List data = new ArrayList<>(); - CdsData mockCdsData = mock(CdsData.class); Map entity = new HashMap<>(); List> attachments = new ArrayList<>(); Map attachment1 = new HashMap<>(); @@ -82,11 +81,15 @@ public void testIsFileNameDuplicateInDrafts() { attachment2.put("repositoryId", "repo1"); attachments.add(attachment1); attachments.add(attachment2); - entity.put("attachments", attachments); - when(mockCdsData.get("attachments")).thenReturn(attachments); // Correctly mock get method - data.add(mockCdsData); - Set duplicateFilenames = SDMUtils.isFileNameDuplicateInDrafts(data, "attachments"); + // Create the nested structure that fetchAttachments expects + Map entityData = new HashMap<>(); + entityData.put("attachmentCompositionName", attachments); + entity.put("entity", entityData); + data.add(CdsData.create(entity)); + + Set duplicateFilenames = + SDMUtils.isFileNameDuplicateInDrafts(data, "attachmentCompositionName", "entity"); assertTrue(duplicateFilenames.contains("file1.txt")); } @@ -640,7 +643,6 @@ void testElementWithoutAnnotation() { void testElementWithAnnotation() { CdsEntity entity = mock(CdsEntity.class); CdsElement element = mock(CdsElement.class); - @SuppressWarnings("unchecked") CdsAnnotation annotation = mock(CdsAnnotation.class); when(annotation.getValue()).thenReturn("name");
This utility method creates a new map with the target entity name as the key and the + * provided entity data as the value. This is necessary because the root of the target entity in + * the CdsData object is not mentioned explicitly, and hence interferes with the recursive + * fetching of attachment compositions. + * + * @param root the entity data structure to be wrapped + * @param targetEntity the name to use as the parent key for wrapping the entity data + * @return a new map containing the target entity name as key and the root entity data as value + */ + public static Map wrapEntityWithParent( + Map root, String targetEntity) { + Map wrapper = new HashMap<>(); + wrapper.put(targetEntity, root); + return wrapper; + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMApplicationHandlerHelper.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMApplicationHandlerHelper.java new file mode 100644 index 000000000..6bb0824a0 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMApplicationHandlerHelper.java @@ -0,0 +1,26 @@ +package com.sap.cds.sdm.handler.common; + +import com.sap.cds.reflect.CdsStructuredType; + +/** + * The class {@link SDMApplicationHandlerHelper} provides helper methods for the SDM attachment + * application handlers. + */ +public final class SDMApplicationHandlerHelper { + private static final String ANNOTATION_IS_MEDIA_DATA = "_is_media_data"; + + /** + * Checks if the entity is a media entity. A media entity is an entity that is annotated with the + * annotation "_is_media_data". + * + * @param baseEntity The entity to check + * @return true if the entity is a media entity, false otherwise + */ + public static boolean isMediaEntity(CdsStructuredType baseEntity) { + return baseEntity.getAnnotationValue(ANNOTATION_IS_MEDIA_DATA, false); + } + + private SDMApplicationHandlerHelper() { + // avoid instantiation + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationCascader.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationCascader.java new file mode 100644 index 000000000..48e8c2734 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationCascader.java @@ -0,0 +1,97 @@ +package com.sap.cds.sdm.handler.common; + +import com.sap.cds.reflect.CdsAssociationType; +import com.sap.cds.reflect.CdsElementDefinition; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.reflect.CdsModel; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +/** + * The class {@link SDMAssociationCascader} is used to find entity paths to all media resource + * entities for a given data model. The path information is returned in a node tree which starts + * from the given entity. Only composition associations are considered. + */ +public class SDMAssociationCascader { + + public SDMNodeTree findEntityPath(CdsModel model, CdsEntity entity) { + var firstList = new LinkedList(); + var internalResultList = + getAttachmentAssociationPath( + model, entity, "", firstList, new ArrayList<>(List.of(entity.getQualifiedName()))); + + var rootTree = new SDMNodeTree(new SDMAssociationIdentifier("", entity.getQualifiedName())); + internalResultList.forEach(rootTree::addPath); + return rootTree; + } + + private List> getAttachmentAssociationPath( + CdsModel model, + CdsEntity entity, + String associationName, + LinkedList firstList, + List processedEntities) { + var internalResultList = new ArrayList>(); + var currentList = new AtomicReference>(); + var localProcessEntities = new ArrayList(); + currentList.set(new LinkedList<>()); + + var isMediaEntity = SDMApplicationHandlerHelper.isMediaEntity(entity); + if (isMediaEntity) { + var identifier = new SDMAssociationIdentifier(associationName, entity.getQualifiedName()); + firstList.addLast(identifier); + } + + if (isMediaEntity) { + internalResultList.add(firstList); + return internalResultList; + } + + Map associations = + entity + .elements() + .filter( + element -> + element.getType().isAssociation() + && element.getType().as(CdsAssociationType.class).isComposition()) + .collect( + Collectors.toMap( + CdsElementDefinition::getName, + element -> element.getType().as(CdsAssociationType.class).getTarget())); + + if (associations.isEmpty()) { + return internalResultList; + } + + var newListNeeded = false; + for (var associatedElement : associations.entrySet()) { + if (!processedEntities.contains(associatedElement.getValue().getQualifiedName())) { + if (newListNeeded) { + currentList.set(new LinkedList<>()); + currentList.get().addAll(firstList); + processedEntities = localProcessEntities; + } else { + firstList.add(new SDMAssociationIdentifier(associationName, entity.getQualifiedName())); + currentList.get().addAll(firstList); + localProcessEntities = new ArrayList<>(processedEntities); + } + processedEntities.add(associatedElement.getValue().getQualifiedName()); + newListNeeded = true; + var result = + getAttachmentAssociationPath( + model, + associatedElement.getValue(), + associatedElement.getKey(), + currentList.get(), + processedEntities); + internalResultList.addAll(result); + } + } + + return internalResultList; + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationIdentifier.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationIdentifier.java new file mode 100644 index 000000000..7ee462949 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAssociationIdentifier.java @@ -0,0 +1,10 @@ +package com.sap.cds.sdm.handler.common; + +/** + * This record is a simple data class that holds the association name and the full entity name for + * SDM attachment processing. + * + * @param associationName the association name + * @param fullEntityName the full entity name + */ +record SDMAssociationIdentifier(String associationName, String fullEntityName) {} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAttachmentsReader.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAttachmentsReader.java new file mode 100644 index 000000000..1164cdf23 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMAttachmentsReader.java @@ -0,0 +1,113 @@ +package com.sap.cds.sdm.handler.common; + +import static java.util.Objects.requireNonNull; + +import com.sap.cds.Result; +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments; +import com.sap.cds.ql.CQL; +import com.sap.cds.ql.Expand; +import com.sap.cds.ql.Select; +import com.sap.cds.ql.StructuredType; +import com.sap.cds.ql.cqn.CqnFilterableStatement; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.reflect.CdsModel; +import com.sap.cds.services.persistence.PersistenceService; +import java.util.ArrayList; +import java.util.List; + +/** + * The class {@link SDMAttachmentsReader} is used to deep read attachments from the database for a + * determined path from the given entity to the media entity. The class uses the {@link + * SDMAssociationCascader} to find the entity path. + * + * The returned data is deep including the path structure to the media entity. + */ +public class SDMAttachmentsReader { + + private final SDMAssociationCascader cascader; + private final PersistenceService persistence; + + public SDMAttachmentsReader(SDMAssociationCascader cascader, PersistenceService persistence) { + this.cascader = requireNonNull(cascader, "cascader must not be null"); + this.persistence = requireNonNull(persistence, "persistence must not be null"); + } + + public List readAttachments( + CdsModel model, CdsEntity entity, CqnFilterableStatement statement) { + + SDMNodeTree nodePath = cascader.findEntityPath(model, entity); + List> expandList = buildExpandList(nodePath); + + Select> select; + if (!expandList.isEmpty()) { + select = Select.from(statement.ref()).columns(expandList); + } else { + select = Select.from(statement.ref()).columns(StructuredType::_all); + } + + if (statement.where().isPresent()) { + select.where(statement.where().get()); + } + + Result result = persistence.run(select); + return result.listOf(Attachments.class); + } + + public List getAttachmentEntityPaths(CdsModel model, CdsEntity entity) { + SDMNodeTree nodePath = cascader.findEntityPath(model, entity); + + List attachmentPaths = new ArrayList<>(); + + if (nodePath != null) { + collectAttachmentPaths(nodePath, attachmentPaths, model); + } + return attachmentPaths; + } + + private void collectAttachmentPaths( + SDMNodeTree node, List attachmentPaths, CdsModel model) { + String entityName = node.getIdentifier().fullEntityName(); + + // Check if this entity is an attachment entity + if (isAttachmentEntity(model, entityName)) { + attachmentPaths.add(entityName); + } + + // Recursively check children + for (SDMNodeTree child : node.getChildren()) { + collectAttachmentPaths(child, attachmentPaths, model); + } + } + + private boolean isAttachmentEntity(CdsModel model, String entityName) { + var entityOpt = model.findEntity(entityName); + if (!entityOpt.isPresent()) { + return false; + } + + CdsEntity entity = entityOpt.get(); + // Check if this entity has the @_is_media_data annotation (indicating attachment entity) + return entity.getAnnotationValue("_is_media_data", false); + } + + private List> buildExpandList(SDMNodeTree root) { + List> expandResultList = new ArrayList<>(); + root.getChildren() + .forEach( + child -> { + Expand> expand = buildExpandFromTree(child); + expandResultList.add(expand); + }); + + return expandResultList; + } + + private Expand> buildExpandFromTree(SDMNodeTree node) { + if (node.getChildren().isEmpty()) { + return CQL.to(node.getIdentifier().associationName()).expand(); + } else { + return CQL.to(node.getIdentifier().associationName()) + .expand(node.getChildren().stream().map(this::buildExpandFromTree).toList()); + } + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMNodeTree.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMNodeTree.java new file mode 100644 index 000000000..71611bef2 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMNodeTree.java @@ -0,0 +1,65 @@ +package com.sap.cds.sdm.handler.common; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * The class {@link SDMNodeTree} is a tree data structure that holds the SDM association identifier + * and its children for attachment processing. + */ +class SDMNodeTree { + + private final SDMAssociationIdentifier identifier; + private final List children = new ArrayList<>(); + + SDMNodeTree(SDMAssociationIdentifier identifier) { + this.identifier = identifier; + } + + void addPath(List path) { + var currentIdentifierOptional = + path.stream() + .filter(entry -> entry.fullEntityName().equals(identifier.fullEntityName())) + .findAny(); + if (currentIdentifierOptional.isEmpty()) { + return; + } + var currentNode = this; + var index = path.indexOf(currentIdentifierOptional.get()); + if (index == path.size() - 1) { + return; + } + for (var i = index + 1; i < path.size(); i++) { + var pathEntry = path.get(i); + currentNode = currentNode.getChildOrNew(pathEntry); + } + } + + private SDMNodeTree getChildOrNew(SDMAssociationIdentifier identifier) { + var childOptional = + children.stream() + .filter(child -> child.identifier.fullEntityName().equals(identifier.fullEntityName())) + .findAny(); + if (childOptional.isPresent()) { + return childOptional.get(); + } else { + SDMNodeTree child = new SDMNodeTree(identifier); + children.add(child); + return child; + } + } + + SDMAssociationIdentifier getIdentifier() { + return identifier; + } + + List getChildren() { + return Collections.unmodifiableList(children); + } + + @Override + public String toString() { + return "SDMNodeTree{" + "identifier=" + identifier + ", children=" + children + '}'; + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java index c5c95242c..1098a1eea 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java @@ -188,9 +188,9 @@ private String getUpIdKey(CdsEntity attachmentDraftEntity) { if (upAssociation.isPresent()) { CdsElement association = upAssociation.get(); // get association type - CdsAssociationType assocType = association.getType(); + CdsAssociationType associationType = association.getType(); // get the refs of the association - List fkElements = assocType.refs().map(ref -> "up__" + ref.path()).toList(); + List fkElements = associationType.refs().map(ref -> "up__" + ref.path()).toList(); upIdKey = fkElements.get(0); } return upIdKey; diff --git a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java index b959d24e3..4cedef71b 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java @@ -1,5 +1,7 @@ package com.sap.cds.sdm.service.handler; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.sap.cds.Result; import com.sap.cds.feature.attachments.service.AttachmentService; import com.sap.cds.ql.Insert; @@ -136,9 +138,7 @@ private void createLink(EventContext context) throws IOException { String upIdKey = attachmentDraftEntity.isPresent() ? getUpIdKey(attachmentDraftEntity.get()) : "up__ID"; CqnSelect select = (CqnSelect) context.get("cqn"); - CqnAnalyzer cqnAnalyzer = CqnAnalyzer.create(cdsModel); - String id = upIdKey.replaceFirst("^up__", ""); - String upID = cqnAnalyzer.analyze(select).rootKeys().get(id).toString(); + String upID = fetchUPIDFromCQN(select); String filenameInRequest = context.get("name").toString(); Result result = @@ -214,9 +214,9 @@ private String getUpIdKey(CdsEntity attachmentDraftEntity) { if (upAssociation.isPresent()) { CdsElement association = upAssociation.get(); // get association type - CdsAssociationType assocType = association.getType(); + CdsAssociationType associationType = association.getType(); // get the refs of the association - List fkElements = assocType.refs().map(ref -> "up__" + ref.path()).toList(); + List fkElements = associationType.refs().map(ref -> "up__" + ref.path()).toList(); upIdKey = fkElements.get(0); } return upIdKey; @@ -308,14 +308,45 @@ private void handleCreateLinkResult( + ":" + context.getTarget()); - var insert = Insert.into(context.getTarget().getQualifiedName()).entry(updatedFields); - for (DraftService draftS : draftService) { - // Process each draftService object - if (context.getTarget().getQualifiedName().contains(draftS.getName())) { - draftS.newDraft(insert); + try { + var insert = Insert.into(context.getTarget().getQualifiedName()).entry(updatedFields); + for (DraftService draftS : draftService) { + if (context.getTarget().getQualifiedName().contains(draftS.getName())) { + draftS.newDraft(insert); + } } + } catch (Exception e) { + logger.info("Exception in insert : " + e.getMessage()); } context.setCompleted(); } } + + private String fetchUPIDFromCQN(CqnSelect select) { + try { + String upID = null; + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = mapper.readTree(select.toString()); + JsonNode refArray = root.path("SELECT").path("from").path("ref"); + JsonNode secondLast = refArray.get(refArray.size() - 2); + JsonNode whereArray = secondLast.path("where"); + for (int i = 0; i < whereArray.size(); i++) { + JsonNode node = whereArray.get(i); + if (node.has("ref") + && node.get("ref").isArray() + && node.get("ref").get(0).asText().equals("ID")) { + JsonNode valNode = whereArray.get(i + 2); + upID = valNode.path("val").asText(); + break; + } + } + if (upID == null) { + throw new ServiceException(SDMConstants.ENTITY_PROCESSING_ERROR_LINK); + } + return upID; + } catch (Exception e) { + logger.error(SDMConstants.ENTITY_PROCESSING_ERROR_LINK, e); + throw new ServiceException(SDMConstants.ENTITY_PROCESSING_ERROR_LINK, e); + } + } } diff --git a/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java b/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java index bf073caba..c6896b2de 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java +++ b/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java @@ -6,6 +6,7 @@ import com.sap.cds.reflect.CdsEntity; import com.sap.cds.sdm.caching.CacheConfig; import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.sdm.handler.applicationservice.helper.AttachmentsHandlerUtils; import com.sap.cds.sdm.model.AttachmentInfo; import com.sap.cds.services.persistence.PersistenceService; import java.io.IOException; @@ -32,11 +33,13 @@ private SDMUtils() { // Doesn't do anything } - public static Set isFileNameDuplicateInDrafts(List data, String composition) { + public static Set isFileNameDuplicateInDrafts( + List data, String composition, String targetEntity) { Set uniqueFilenames = new HashSet<>(); Set duplicateFilenames = new HashSet<>(); for (Map entity : data) { - List> attachments = (List>) entity.get(composition); + List> attachments = + AttachmentsHandlerUtils.fetchAttachments(targetEntity, entity, composition); if (attachments != null) { Iterator> iterator = attachments.iterator(); while (iterator.hasNext()) { diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java b/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java index 13d568e67..5a35fd04a 100644 --- a/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java @@ -530,7 +530,7 @@ public String renameAttachment( .build(); try (Response renameResponse = httpClient.newCall(request).execute()) { - if (renameResponse.code() != 200) { + if (!renameResponse.isSuccessful()) { System.out.println( "Rename Attachment failed in the " + facetName diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java b/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java index e961dc944..744b13183 100644 --- a/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java @@ -504,7 +504,7 @@ public String renameAttachment( .build(); try (Response renameResponse = httpClient.newCall(request).execute()) { - if (renameResponse.code() != 200) { + if (!renameResponse.isSuccessful()) { throw new IOException("Attachment was not renamed in section: " + facetName); } return "Renamed"; diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java b/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java index 0fcc83c38..3f97a8b7e 100644 --- a/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java @@ -604,8 +604,8 @@ void testRenameEntitiesWithUnsupportedCharacter() { counter = -1; // Reset counter for the next check response = api.saveEntityDraft(appUrl, entityName, srvpath, entityID); String expected = - "[{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 sample/1234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," - + "{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 reference1/234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," + "[{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 reference1/234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," + + "{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 sample/1234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," + "{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 footnote1/234\\n\\nRename the files and try again.\",\"numericSeverity\":3}]"; if (response.equals(expected)) { testStatus = true; @@ -673,7 +673,7 @@ void testRenameSingleDuplicate() { + "{\"code\":\"\",\"message\":\"The file(s) %s have been added multiple times. Please rename and try again.\",\"@Common.numericSeverity\":4}," + "{\"code\":\"\",\"message\":\"The file(s) %s have been added multiple times. Please rename and try again.\",\"@Common.numericSeverity\":4}" + "]}}", - name[0], name[1], name[2]); + name[1], name[0], name[2]); if (response.equals(expected)) { for (int i = 0; i < facet.length; i++) { // Attempt to rename again with a different name @@ -757,7 +757,7 @@ void testRenameEntitiesWithoutSDMRole() throws IOException { + // "\\n" + // - "\\t\\u2022 sample123\\n" + "\\t\\u2022 reference123\\n" + // "\\n" + // @@ -765,7 +765,7 @@ void testRenameEntitiesWithoutSDMRole() throws IOException { + // "\\n" + // - "\\t\\u2022 reference123\\n" + "\\t\\u2022 sample123\\n" + // "\\n" + // 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 03db40a13..dd0b4711f 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 @@ -9,8 +9,10 @@ import com.sap.cds.CdsData; import com.sap.cds.reflect.*; +import com.sap.cds.sdm.caching.CacheConfig; import com.sap.cds.sdm.handler.TokenHandler; import com.sap.cds.sdm.handler.applicationservice.SDMCreateAttachmentsHandler; +import com.sap.cds.sdm.handler.applicationservice.helper.AttachmentsHandlerUtils; import com.sap.cds.sdm.model.SDMCredentials; import com.sap.cds.sdm.persistence.DBQuery; import com.sap.cds.sdm.service.SDMService; @@ -24,7 +26,7 @@ import com.sap.cds.services.request.UserInfo; import java.io.IOException; import java.util.*; -import java.util.stream.Stream; +import org.ehcache.Cache; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -81,46 +83,58 @@ public void tearDown() { @Test public void testProcessBefore() throws IOException { - // Arrange the mock compositions scenario - List expectedCompositionNames = Arrays.asList("Name1", "Name2"); - - // Create a Stream of mocked CdsElement instances - Stream compositionsStream = Stream.of(cdsElement, cdsElement); - - when(context.getTarget().compositions()).thenReturn(compositionsStream); - when(cdsElement.getType()).thenReturn(cdsAssociationType); - when(cdsAssociationType.getTargetAspect()).thenReturn(Optional.of(targetAspect)); - when(cdsAssociationType.getTargetAspect().get().getQualifiedName()) - .thenReturn("sap.attachments.Attachments"); - when(cdsElement.getName()).thenReturn("Name1").thenReturn("Name2"); - - List dataList = new ArrayList<>(); - - // Act - handler.processBefore(context, dataList); - - // Assert that updateName was called with the compositions detected - for (String compositionName : expectedCompositionNames) { - verify(handler).updateName(context, dataList, compositionName); + try (MockedStatic attachmentsHandlerUtilsMocked = + mockStatic(AttachmentsHandlerUtils.class)) { + // Arrange the mock compositions scenario + Map expectedCompositionMapping = new HashMap<>(); + expectedCompositionMapping.put("Name1", "Name1"); + expectedCompositionMapping.put("Name2", "Name2"); + + // Mock AttachmentsHandlerUtils.getAttachmentPathMapping to return the expected mapping + attachmentsHandlerUtilsMocked + .when(() -> AttachmentsHandlerUtils.getAttachmentPathMapping(any(), any(), any())) + .thenReturn(expectedCompositionMapping); + + List dataList = new ArrayList<>(); + + // Act + handler.processBefore(context, dataList); + + // Assert that updateName was called with the compositions detected + for (Map.Entry entry : expectedCompositionMapping.entrySet()) { + verify(handler).updateName(context, dataList, entry.getKey(), entry.getValue()); + } } } @Test public void testUpdateNameWithDuplicateFilenames() throws IOException { - // Arrange - List data = new ArrayList<>(); - Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); - sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(duplicateFilenames); - - // Act - handler.updateName(context, data, "composition"); - - // Assert - verify(messages, times(1)) - .error( - "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Arrange + List data = new ArrayList<>(); + Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); + when(context.getMessages()).thenReturn(messages); + + // Mock the target entity + CdsEntity targetEntity = mock(CdsEntity.class); + when(targetEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getTarget()).thenReturn(targetEntity); + + sdmUtilsMockedStatic + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "compositionName", "TestEntity")) + .thenReturn(duplicateFilenames); + + // Act + handler.updateName(context, data, "compositionDefinition", "compositionName"); + + // Assert + verify(messages, times(1)) + .error( + "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); + } } @Test @@ -128,11 +142,11 @@ public void testUpdateNameWithEmptyData() throws IOException { // Arrange List data = new ArrayList<>(); sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "compositionName", "entity")) .thenReturn(Collections.emptySet()); // Act - handler.updateName(context, data, ""); + handler.updateName(context, data, "compositionDefinition", "compositionName"); // Assert verify(messages, never()).error(anyString()); @@ -141,32 +155,37 @@ public void testUpdateNameWithEmptyData() throws IOException { @Test public void testUpdateNameWithNoAttachments() throws IOException { - // Arrange - List data = new ArrayList<>(); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); - // Create an entity map without any attachments - Map entity = new HashMap<>(); + // Arrange + List data = new ArrayList<>(); - // Wrap the entity map in CdsData - CdsData cdsDataEntity = CdsData.create(entity); + // Create an entity map without any attachments + Map entity = new HashMap<>(); - // Add the CdsData entity to the data list - data.add(cdsDataEntity); + // Wrap the entity map in CdsData + CdsData cdsDataEntity = CdsData.create(entity); - // Mock utility methods - sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(Collections.emptySet()); + // Add the CdsData entity to the data list + data.add(cdsDataEntity); - // Act - handler.updateName(context, data, ""); + // Mock utility methods + sdmUtilsMockedStatic + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "compositionName", "entity")) + .thenReturn(Collections.emptySet()); - // Assert that no updateAttachments calls were made, as there are no attachments - verify(sdmService, never()).updateAttachments(any(), any(), any(), any(), anyBoolean()); + // Act + handler.updateName(context, data, "compositionDefinition", "compositionName"); - // Assert that no error or warning messages were logged - verify(messages, never()).error(anyString()); - verify(messages, never()).warn(anyString()); + // Assert that no updateAttachments calls were made, as there are no attachments + verify(sdmService, never()).updateAttachments(any(), any(), any(), any(), anyBoolean()); + + // Assert that no error or warning messages were logged + verify(messages, never()).error(anyString()); + verify(messages, never()).warn(anyString()); + } } // @Test @@ -383,88 +402,110 @@ public void testUpdateNameWithNoAttachments() throws IOException { // } @Test public void testUpdateNameWithEmptyFilename() throws IOException { - List data = new ArrayList<>(); - Map entity = new HashMap<>(); - List> attachments = new ArrayList<>(); - - Map attachment = new HashMap<>(); - attachment.put("ID", "test-id"); - attachment.put("fileName", null); // Empty filename - attachment.put("objectId", "test-object-id"); - attachments.add(attachment); - - // entity.put("attachments", attachments); - entity.put("composition", attachments); - - CdsData cdsDataEntity = CdsData.create(entity); // Wrap entity in CdsData - data.add(cdsDataEntity); // Add to data - - // Mock duplicate file name - sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(new HashSet<>()); - - // Mock attachment entity - CdsEntity attachmentDraftEntity = mock(CdsEntity.class); - when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); - when(context.getTarget()).thenReturn(attachmentDraftEntity); - when(context.getModel()).thenReturn(model); - - // Mock findEntity to return an optional containing attachmentDraftEntity - when(model.findEntity("some.qualified.Name" + "." + "composition")) - .thenReturn(Optional.of(attachmentDraftEntity)); - UserInfo userInfo = Mockito.mock(UserInfo.class); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.isSystemUser()).thenReturn(false); - // Mock authentication - when(context.getMessages()).thenReturn(messages); - when(context.getAuthenticationInfo()).thenReturn(authInfo); - when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); - when(jwtTokenInfo.getToken()).thenReturn("testJwtToken"); - - // Mock getObject - when(sdmService.getObject("test-object-id", mockCredentials, false)) - .thenReturn("fileInSDM.txt"); - - // Mock getSecondaryTypeProperties - Map secondaryTypeProperties = new HashMap<>(); - Map updatedSecondaryProperties = new HashMap<>(); - sdmUtilsMockedStatic - .when( - () -> - SDMUtils.getSecondaryTypeProperties(Optional.of(attachmentDraftEntity), attachment)) - .thenReturn(secondaryTypeProperties); - sdmUtilsMockedStatic - .when( - () -> - SDMUtils.getUpdatedSecondaryProperties( - Optional.of(attachmentDraftEntity), - attachment, - persistenceService, - secondaryTypeProperties, - updatedSecondaryProperties)) - .thenReturn(new HashMap<>()); - - // Mock restricted character - sdmUtilsMockedStatic - .when(() -> SDMUtils.isRestrictedCharactersInName("fileNameInRequest")) - .thenReturn(false); - - when(dbQuery.getAttachmentForID(attachmentDraftEntity, persistenceService, "test-id")) - .thenReturn(null); - - // When getPropertiesForID is called - when(dbQuery.getPropertiesForID( - attachmentDraftEntity, persistenceService, "test-id", secondaryTypeProperties)) - .thenReturn(updatedSecondaryProperties); - - // Act & Assert - ServiceException exception = - assertThrows( - ServiceException.class, () -> handler.updateName(context, data, "composition")); - - // Assert that the correct exception message is returned - assertEquals("Filename cannot be empty", exception.getMessage()); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + + Map attachment = new HashMap<>(); + attachment.put("ID", "test-id"); + attachment.put("fileName", null); // Empty filename + attachment.put("objectId", "test-object-id"); + attachments.add(attachment); + + // entity.put("attachments", attachments); + entity.put("composition", attachments); + + CdsData cdsDataEntity = CdsData.create(entity); // Wrap entity in CdsData + data.add(cdsDataEntity); // Add to data + + // Mock duplicate file name + sdmUtilsMockedStatic + .when( + () -> + SDMUtils.isFileNameDuplicateInDrafts( + data, "compositionName", "some.qualified.Name")) + .thenReturn(new HashSet<>()); + + // Mock AttachmentsHandlerUtils.fetchAttachments to return the attachment with null filename + try (MockedStatic attachmentsHandlerUtilsMocked = + mockStatic(AttachmentsHandlerUtils.class)) { + attachmentsHandlerUtilsMocked + .when( + () -> + AttachmentsHandlerUtils.fetchAttachments( + "some.qualified.Name", entity, "compositionName")) + .thenReturn(attachments); + + // Mock attachment entity + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + + // Mock findEntity to return an optional containing attachmentDraftEntity + when(model.findEntity("compositionDefinition")) + .thenReturn(Optional.of(attachmentDraftEntity)); + UserInfo userInfo = Mockito.mock(UserInfo.class); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.isSystemUser()).thenReturn(false); + // Mock authentication + when(context.getMessages()).thenReturn(messages); + when(context.getAuthenticationInfo()).thenReturn(authInfo); + when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); + when(jwtTokenInfo.getToken()).thenReturn("testJwtToken"); + + // Mock getObject + when(sdmService.getObject("test-object-id", mockCredentials, false)) + .thenReturn("fileInSDM.txt"); + + // Mock getSecondaryTypeProperties + Map secondaryTypeProperties = new HashMap<>(); + Map updatedSecondaryProperties = new HashMap<>(); + sdmUtilsMockedStatic + .when( + () -> + SDMUtils.getSecondaryTypeProperties( + Optional.of(attachmentDraftEntity), attachment)) + .thenReturn(secondaryTypeProperties); + sdmUtilsMockedStatic + .when( + () -> + SDMUtils.getUpdatedSecondaryProperties( + Optional.of(attachmentDraftEntity), + attachment, + persistenceService, + secondaryTypeProperties, + updatedSecondaryProperties)) + .thenReturn(new HashMap<>()); + + // Mock restricted character + sdmUtilsMockedStatic + .when(() -> SDMUtils.isRestrictedCharactersInName("fileNameInRequest")) + .thenReturn(false); + + when(dbQuery.getAttachmentForID(attachmentDraftEntity, persistenceService, "test-id")) + .thenReturn(null); + + // When getPropertiesForID is called + when(dbQuery.getPropertiesForID( + attachmentDraftEntity, persistenceService, "test-id", secondaryTypeProperties)) + .thenReturn(updatedSecondaryProperties); + + // Act & Assert + ServiceException exception = + assertThrows( + ServiceException.class, + () -> + handler.updateName(context, data, "compositionDefinition", "compositionName")); + + // Assert that the correct exception message is returned + assertEquals("Filename cannot be empty", exception.getMessage()); + } // Close AttachmentsHandlerUtils mock + } } // @Test 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 2c9cf5259..30a550235 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 @@ -9,9 +9,11 @@ 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.SDMUpdateAttachmentsHandler; +import com.sap.cds.sdm.handler.applicationservice.helper.AttachmentsHandlerUtils; import com.sap.cds.sdm.model.CmisDocument; import com.sap.cds.sdm.model.SDMCredentials; import com.sap.cds.sdm.persistence.DBQuery; @@ -26,7 +28,7 @@ import com.sap.cds.services.request.UserInfo; import java.io.IOException; import java.util.*; -import java.util.stream.Stream; +import org.ehcache.Cache; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.*; @@ -74,54 +76,69 @@ public void tearDown() { @Test public void testProcessBefore() throws IOException { - // Arrange - List expectedCompositionNames = Arrays.asList("Name1", "Name2"); - - // Simulate a stream of CdsElement instances returned from the mock target's compositions - Stream compositionsStream = Stream.of(cdsElement, cdsElement); + try (MockedStatic attachmentsHandlerUtilsMocked = + mockStatic(AttachmentsHandlerUtils.class); + MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Arrange the mock compositions scenario + Map expectedCompositionMapping = new HashMap<>(); + expectedCompositionMapping.put("Name1", "Name1"); + expectedCompositionMapping.put("Name2", "Name2"); + + // Mock context.getTarget() and context.getModel() + when(context.getTarget()).thenReturn(targetEntity); + when(targetEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getModel()).thenReturn(model); + when(model.findEntity(anyString())).thenReturn(Optional.of(targetEntity)); + + // Mock AttachmentsHandlerUtils.getAttachmentPathMapping to return the expected mapping + attachmentsHandlerUtilsMocked + .when(() -> AttachmentsHandlerUtils.getAttachmentPathMapping(any(), any(), any())) + .thenReturn(expectedCompositionMapping); + + List dataList = new ArrayList<>(); + + // Act + handler.processBefore(context, dataList); + + // Assert that updateName was called with the compositions detected + for (Map.Entry entry : expectedCompositionMapping.entrySet()) { + verify(handler).updateName(context, dataList, entry.getKey(), entry.getValue()); + } + } + } - // mock the target and model of the context - when(context.getTarget()).thenReturn(targetEntity); - when(targetEntity.compositions()).thenReturn(compositionsStream); - when(context.getModel()).thenReturn(model); + @Test + public void testRenameWithDuplicateFilenames() throws IOException { + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); - // Mock findEntity to return an optional containing attachmentDraftEntity - when(model.findEntity(anyString())).thenReturn(Optional.of(targetEntity)); + List data = new ArrayList<>(); + Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); + when(context.getMessages()).thenReturn(messages); - // Mock the elements and their associations - when(cdsElement.getType()).thenReturn(cdsAssociationType); - when(cdsAssociationType.getTargetAspect()).thenReturn(Optional.of(targetAspect)); - when(targetAspect.getQualifiedName()).thenReturn("sap.attachments.Attachments"); - when(cdsElement.getName()).thenReturn("Name1").thenReturn("Name2"); + // Mock the target entity + CdsEntity targetEntity = mock(CdsEntity.class); + when(targetEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getTarget()).thenReturn(targetEntity); - List dataList = new ArrayList<>(); + sdmUtilsMockedStatic = mockStatic(SDMUtils.class); + sdmUtilsMockedStatic + .when(() -> isFileNameDuplicateInDrafts(data, "compositionName", "TestEntity")) + .thenReturn(duplicateFilenames); - // Act - handler.processBefore(context, dataList); + handler.updateName(context, data, "compositionDefinition", "compositionName"); - // Assert that updateName was called with the compositions detected - for (String compositionName : expectedCompositionNames) { - verify(handler).updateName(context, dataList, compositionName); + verify(messages, times(1)) + .error( + "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); } } - @Test - public void testRenameWithDuplicateFilenames() throws IOException { - List data = new ArrayList<>(); - Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); - when(context.getMessages()).thenReturn(messages); - sdmUtilsMockedStatic = mockStatic(SDMUtils.class); - sdmUtilsMockedStatic - .when(() -> isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(duplicateFilenames); - - handler.updateName(context, data, "composition"); - - verify(messages, times(1)) - .error( - "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); - } - // @Test // public void testRenameWithUniqueFilenames() throws IOException { // List data = prepareMockAttachmentData("file1.txt"); @@ -212,62 +229,125 @@ public void testRenameWithDuplicateFilenames() throws IOException { @Test public void testRenameWithNoSDMRoles() throws IOException { - // Mock the data structure to simulate the attachments - List data = new ArrayList<>(); - Map entity = new HashMap<>(); - List> attachments = new ArrayList<>(); - Map attachment = spy(new HashMap<>()); - Map secondaryProperties = new HashMap<>(); - Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); - secondaryProperties.put("filename", "file1.txt"); - - CmisDocument document = new CmisDocument(); - document.setFileName("file1.txt"); - - attachment.put("fileName", "file1.txt"); - attachment.put("url", "objectId"); - attachment.put("ID", "test-id"); - attachments.add(attachment); - - entity.put("attachments", attachments); - CdsData mockCdsData = mock(CdsData.class); - when(mockCdsData.get("composition")).thenReturn(attachments); - data.add(mockCdsData); - - CdsEntity attachmentDraftEntity = mock(CdsEntity.class); - when(context.getTarget()).thenReturn(attachmentDraftEntity); - when(context.getModel()).thenReturn(model); - when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); - when(model.findEntity("some.qualified.Name.composition")) - .thenReturn(Optional.of(attachmentDraftEntity)); - when(context.getMessages()).thenReturn(messages); - UserInfo userInfo = Mockito.mock(UserInfo.class); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.isSystemUser()).thenReturn(false); - when(tokenHandler.getSDMCredentials()).thenReturn(mockCredentials); - when(dbQuery.getAttachmentForID( - any(CdsEntity.class), any(PersistenceService.class), anyString())) - .thenReturn("file123.txt"); - - when(sdmService.updateAttachments( - mockCredentials, - document, - secondaryProperties, - secondaryPropertiesWithInvalidDefinitions, - false)) - .thenReturn(403); // Forbidden + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class); + MockedStatic attachmentsMockStatic = + mockStatic(AttachmentsHandlerUtils.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Mock the data structure to simulate the attachments + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + Map attachment = spy(new HashMap<>()); + Map secondaryProperties = new HashMap<>(); + Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); + secondaryProperties.put("filename", "file1.txt"); - // Call the method - handler.updateName(context, data, "composition"); + CmisDocument document = new CmisDocument(); + document.setFileName("file1.txt"); - // Capture and assert the warning message - ArgumentCaptor warningCaptor = ArgumentCaptor.forClass(String.class); - verify(messages).warn(warningCaptor.capture()); - String warningMessage = warningCaptor.getValue(); + attachment.put("fileName", "file1.txt"); + attachment.put("url", "objectId"); + attachment.put("ID", "test-id"); + attachments.add(attachment); - String expectedMessage = - SDMConstants.noSDMRolesMessage(Collections.singletonList("file123.txt"), "update"); - assertEquals(expectedMessage, warningMessage); + entity.put("compositionName", attachments); + CdsData mockCdsData = mock(CdsData.class); + data.add(mockCdsData); + + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + when(model.findEntity("compositionDefinition")) + .thenReturn(Optional.of(attachmentDraftEntity)); + when(context.getMessages()).thenReturn(messages); + UserInfo userInfo = Mockito.mock(UserInfo.class); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.isSystemUser()).thenReturn(false); + when(tokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + when(dbQuery.getAttachmentForID( + any(CdsEntity.class), any(PersistenceService.class), anyString())) + .thenReturn("file123.txt"); + + when(sdmService.updateAttachments( + mockCredentials, + document, + secondaryProperties, + secondaryPropertiesWithInvalidDefinitions, + false)) + .thenReturn(403); // Forbidden + + // Mock AttachmentsHandlerUtils.fetchAttachments + attachmentsMockStatic + .when( + () -> + AttachmentsHandlerUtils.fetchAttachments( + anyString(), any(Map.class), eq("compositionName"))) + .thenReturn(attachments); + + // Mock SDMUtils methods + try (MockedStatic sdmUtilsMock = mockStatic(SDMUtils.class)) { + sdmUtilsMock + .when( + () -> + SDMUtils.isFileNameDuplicateInDrafts( + any(List.class), eq("compositionName"), anyString())) + .thenReturn(Collections.emptySet()); + + sdmUtilsMock + .when(() -> SDMUtils.getPropertyTitles(any(Optional.class), any(Map.class))) + .thenReturn(Collections.emptyMap()); + + sdmUtilsMock + .when( + () -> + SDMUtils.getSecondaryPropertiesWithInvalidDefinition( + any(Optional.class), any(Map.class))) + .thenReturn(Collections.emptyMap()); + + sdmUtilsMock + .when(() -> SDMUtils.getSecondaryTypeProperties(any(Optional.class), any(Map.class))) + .thenReturn(Collections.emptyMap()); + + sdmUtilsMock + .when( + () -> + SDMUtils.getUpdatedSecondaryProperties( + any(Optional.class), + any(Map.class), + any(PersistenceService.class), + any(Map.class), + any(Map.class))) + .thenReturn(secondaryProperties); + + sdmUtilsMock + .when(() -> SDMUtils.isRestrictedCharactersInName(anyString())) + .thenReturn(false); + + // Call the method + handler.updateName(context, data, "compositionDefinition", "compositionName"); + + // Capture and assert the warning message + ArgumentCaptor warningCaptor = ArgumentCaptor.forClass(String.class); + verify(messages).warn(warningCaptor.capture()); + String warningMessage = warningCaptor.getValue(); + + String expectedMessage = + SDMConstants.noSDMRolesMessage(Collections.singletonList("file123.txt"), "update"); + assertEquals(expectedMessage, warningMessage); + } + + // Capture and assert the warning message + ArgumentCaptor warningCaptor = ArgumentCaptor.forClass(String.class); + verify(messages).warn(warningCaptor.capture()); + String warningMessage = warningCaptor.getValue(); + + String expectedMessage = + SDMConstants.noSDMRolesMessage(Collections.singletonList("file123.txt"), "update"); + assertEquals(expectedMessage, warningMessage); + } } // @Test @@ -409,35 +489,41 @@ public void testRenameWithoutFileInSDM() throws IOException { @Test public void testRenameWithNoAttachments() throws IOException { - // Arrange - List data = new ArrayList<>(); - CdsEntity attachmentDraftEntity = mock(CdsEntity.class); - Map secondaryProperties = new HashMap<>(); - Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); - CmisDocument document = new CmisDocument(); - when(context.getTarget()).thenReturn(attachmentDraftEntity); - when(context.getModel()).thenReturn(model); - - when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); - - String expectedEntityName = "some.qualified.Name.attachments"; - when(model.findEntity(expectedEntityName)).thenReturn(Optional.of(attachmentDraftEntity)); - - CdsData mockCdsData = mock(CdsData.class); - when(mockCdsData.get("attachments")).thenReturn(null); - data.add(mockCdsData); - - // Act - handler.updateName(context, data, "attachments"); - - // Assert - verify(sdmService, never()) - .updateAttachments( - eq(mockCredentials), - eq(document), - eq(secondaryProperties), - eq(secondaryPropertiesWithInvalidDefinitions), - eq(false)); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Arrange + List data = new ArrayList<>(); + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + Map secondaryProperties = new HashMap<>(); + Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); + CmisDocument document = new CmisDocument(); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + + // Mock the correct entity name that the handler will look for + when(model.findEntity("compositionDefinition")) + .thenReturn(Optional.of(attachmentDraftEntity)); + + Map entity = new HashMap<>(); + CdsData cdsDataEntity = CdsData.create(entity); + data.add(cdsDataEntity); + + // Act + handler.updateName(context, data, "compositionDefinition", "compositionName"); + + // Assert + verify(sdmService, never()) + .updateAttachments( + eq(mockCredentials), + eq(document), + eq(secondaryProperties), + eq(secondaryPropertiesWithInvalidDefinitions), + eq(false)); + } } // @Test diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java index 0a3f98001..7ef6443d5 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java @@ -157,6 +157,9 @@ void testCreate_shouldCreateLink() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -249,6 +252,9 @@ void testCreate_ShouldThrowSpecifiedExceptionWhenMaxCountReached() throws IOExce when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -307,6 +313,9 @@ void testCreate_ShouldThrowDefaultExceptionWhenMaxCountReached() throws IOExcept when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -365,6 +374,9 @@ void testCreate_ShouldThrowExceptionWhenRestrictedCharacterInLinkName() throws I when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("test/URL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -425,6 +437,9 @@ void testCreate_ThrowsServiceExceptionOnDuplicateFile() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -486,6 +501,9 @@ void testCreate_ThrowsServiceException_WhenCreateDocumentThrowsException() throw when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -554,6 +572,9 @@ void testCreate_ThrowsServiceExceptionOnDuplicateStatus() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -623,6 +644,9 @@ void testCreate_ThrowsServiceExceptionOnFailStatus() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -691,6 +715,9 @@ void testCreate_ThrowsServiceExceptionOnUnauthorizedStatus() throws IOException when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java index fa7f35387..dedb2f5ec 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java @@ -71,7 +71,6 @@ private void setUp() { @Test public void testIsFileNameDuplicateInDrafts() { List data = new ArrayList<>(); - CdsData mockCdsData = mock(CdsData.class); Map entity = new HashMap<>(); List> attachments = new ArrayList<>(); Map attachment1 = new HashMap<>(); @@ -82,11 +81,15 @@ public void testIsFileNameDuplicateInDrafts() { attachment2.put("repositoryId", "repo1"); attachments.add(attachment1); attachments.add(attachment2); - entity.put("attachments", attachments); - when(mockCdsData.get("attachments")).thenReturn(attachments); // Correctly mock get method - data.add(mockCdsData); - Set duplicateFilenames = SDMUtils.isFileNameDuplicateInDrafts(data, "attachments"); + // Create the nested structure that fetchAttachments expects + Map entityData = new HashMap<>(); + entityData.put("attachmentCompositionName", attachments); + entity.put("entity", entityData); + data.add(CdsData.create(entity)); + + Set duplicateFilenames = + SDMUtils.isFileNameDuplicateInDrafts(data, "attachmentCompositionName", "entity"); assertTrue(duplicateFilenames.contains("file1.txt")); } @@ -640,7 +643,6 @@ void testElementWithoutAnnotation() { void testElementWithAnnotation() { CdsEntity entity = mock(CdsEntity.class); CdsElement element = mock(CdsElement.class); - @SuppressWarnings("unchecked") CdsAnnotation annotation = mock(CdsAnnotation.class); when(annotation.getValue()).thenReturn("name");
true
false
The returned data is deep including the path structure to the media entity. + */ +public class SDMAttachmentsReader { + + private final SDMAssociationCascader cascader; + private final PersistenceService persistence; + + public SDMAttachmentsReader(SDMAssociationCascader cascader, PersistenceService persistence) { + this.cascader = requireNonNull(cascader, "cascader must not be null"); + this.persistence = requireNonNull(persistence, "persistence must not be null"); + } + + public List readAttachments( + CdsModel model, CdsEntity entity, CqnFilterableStatement statement) { + + SDMNodeTree nodePath = cascader.findEntityPath(model, entity); + List> expandList = buildExpandList(nodePath); + + Select> select; + if (!expandList.isEmpty()) { + select = Select.from(statement.ref()).columns(expandList); + } else { + select = Select.from(statement.ref()).columns(StructuredType::_all); + } + + if (statement.where().isPresent()) { + select.where(statement.where().get()); + } + + Result result = persistence.run(select); + return result.listOf(Attachments.class); + } + + public List getAttachmentEntityPaths(CdsModel model, CdsEntity entity) { + SDMNodeTree nodePath = cascader.findEntityPath(model, entity); + + List attachmentPaths = new ArrayList<>(); + + if (nodePath != null) { + collectAttachmentPaths(nodePath, attachmentPaths, model); + } + return attachmentPaths; + } + + private void collectAttachmentPaths( + SDMNodeTree node, List attachmentPaths, CdsModel model) { + String entityName = node.getIdentifier().fullEntityName(); + + // Check if this entity is an attachment entity + if (isAttachmentEntity(model, entityName)) { + attachmentPaths.add(entityName); + } + + // Recursively check children + for (SDMNodeTree child : node.getChildren()) { + collectAttachmentPaths(child, attachmentPaths, model); + } + } + + private boolean isAttachmentEntity(CdsModel model, String entityName) { + var entityOpt = model.findEntity(entityName); + if (!entityOpt.isPresent()) { + return false; + } + + CdsEntity entity = entityOpt.get(); + // Check if this entity has the @_is_media_data annotation (indicating attachment entity) + return entity.getAnnotationValue("_is_media_data", false); + } + + private List> buildExpandList(SDMNodeTree root) { + List> expandResultList = new ArrayList<>(); + root.getChildren() + .forEach( + child -> { + Expand> expand = buildExpandFromTree(child); + expandResultList.add(expand); + }); + + return expandResultList; + } + + private Expand> buildExpandFromTree(SDMNodeTree node) { + if (node.getChildren().isEmpty()) { + return CQL.to(node.getIdentifier().associationName()).expand(); + } else { + return CQL.to(node.getIdentifier().associationName()) + .expand(node.getChildren().stream().map(this::buildExpandFromTree).toList()); + } + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMNodeTree.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMNodeTree.java new file mode 100644 index 000000000..71611bef2 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMNodeTree.java @@ -0,0 +1,65 @@ +package com.sap.cds.sdm.handler.common; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * The class {@link SDMNodeTree} is a tree data structure that holds the SDM association identifier + * and its children for attachment processing. + */ +class SDMNodeTree { + + private final SDMAssociationIdentifier identifier; + private final List children = new ArrayList<>(); + + SDMNodeTree(SDMAssociationIdentifier identifier) { + this.identifier = identifier; + } + + void addPath(List path) { + var currentIdentifierOptional = + path.stream() + .filter(entry -> entry.fullEntityName().equals(identifier.fullEntityName())) + .findAny(); + if (currentIdentifierOptional.isEmpty()) { + return; + } + var currentNode = this; + var index = path.indexOf(currentIdentifierOptional.get()); + if (index == path.size() - 1) { + return; + } + for (var i = index + 1; i < path.size(); i++) { + var pathEntry = path.get(i); + currentNode = currentNode.getChildOrNew(pathEntry); + } + } + + private SDMNodeTree getChildOrNew(SDMAssociationIdentifier identifier) { + var childOptional = + children.stream() + .filter(child -> child.identifier.fullEntityName().equals(identifier.fullEntityName())) + .findAny(); + if (childOptional.isPresent()) { + return childOptional.get(); + } else { + SDMNodeTree child = new SDMNodeTree(identifier); + children.add(child); + return child; + } + } + + SDMAssociationIdentifier getIdentifier() { + return identifier; + } + + List getChildren() { + return Collections.unmodifiableList(children); + } + + @Override + public String toString() { + return "SDMNodeTree{" + "identifier=" + identifier + ", children=" + children + '}'; + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java index c5c95242c..1098a1eea 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java @@ -188,9 +188,9 @@ private String getUpIdKey(CdsEntity attachmentDraftEntity) { if (upAssociation.isPresent()) { CdsElement association = upAssociation.get(); // get association type - CdsAssociationType assocType = association.getType(); + CdsAssociationType associationType = association.getType(); // get the refs of the association - List fkElements = assocType.refs().map(ref -> "up__" + ref.path()).toList(); + List fkElements = associationType.refs().map(ref -> "up__" + ref.path()).toList(); upIdKey = fkElements.get(0); } return upIdKey; diff --git a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java index b959d24e3..4cedef71b 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java @@ -1,5 +1,7 @@ package com.sap.cds.sdm.service.handler; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.sap.cds.Result; import com.sap.cds.feature.attachments.service.AttachmentService; import com.sap.cds.ql.Insert; @@ -136,9 +138,7 @@ private void createLink(EventContext context) throws IOException { String upIdKey = attachmentDraftEntity.isPresent() ? getUpIdKey(attachmentDraftEntity.get()) : "up__ID"; CqnSelect select = (CqnSelect) context.get("cqn"); - CqnAnalyzer cqnAnalyzer = CqnAnalyzer.create(cdsModel); - String id = upIdKey.replaceFirst("^up__", ""); - String upID = cqnAnalyzer.analyze(select).rootKeys().get(id).toString(); + String upID = fetchUPIDFromCQN(select); String filenameInRequest = context.get("name").toString(); Result result = @@ -214,9 +214,9 @@ private String getUpIdKey(CdsEntity attachmentDraftEntity) { if (upAssociation.isPresent()) { CdsElement association = upAssociation.get(); // get association type - CdsAssociationType assocType = association.getType(); + CdsAssociationType associationType = association.getType(); // get the refs of the association - List fkElements = assocType.refs().map(ref -> "up__" + ref.path()).toList(); + List fkElements = associationType.refs().map(ref -> "up__" + ref.path()).toList(); upIdKey = fkElements.get(0); } return upIdKey; @@ -308,14 +308,45 @@ private void handleCreateLinkResult( + ":" + context.getTarget()); - var insert = Insert.into(context.getTarget().getQualifiedName()).entry(updatedFields); - for (DraftService draftS : draftService) { - // Process each draftService object - if (context.getTarget().getQualifiedName().contains(draftS.getName())) { - draftS.newDraft(insert); + try { + var insert = Insert.into(context.getTarget().getQualifiedName()).entry(updatedFields); + for (DraftService draftS : draftService) { + if (context.getTarget().getQualifiedName().contains(draftS.getName())) { + draftS.newDraft(insert); + } } + } catch (Exception e) { + logger.info("Exception in insert : " + e.getMessage()); } context.setCompleted(); } } + + private String fetchUPIDFromCQN(CqnSelect select) { + try { + String upID = null; + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = mapper.readTree(select.toString()); + JsonNode refArray = root.path("SELECT").path("from").path("ref"); + JsonNode secondLast = refArray.get(refArray.size() - 2); + JsonNode whereArray = secondLast.path("where"); + for (int i = 0; i < whereArray.size(); i++) { + JsonNode node = whereArray.get(i); + if (node.has("ref") + && node.get("ref").isArray() + && node.get("ref").get(0).asText().equals("ID")) { + JsonNode valNode = whereArray.get(i + 2); + upID = valNode.path("val").asText(); + break; + } + } + if (upID == null) { + throw new ServiceException(SDMConstants.ENTITY_PROCESSING_ERROR_LINK); + } + return upID; + } catch (Exception e) { + logger.error(SDMConstants.ENTITY_PROCESSING_ERROR_LINK, e); + throw new ServiceException(SDMConstants.ENTITY_PROCESSING_ERROR_LINK, e); + } + } } diff --git a/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java b/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java index bf073caba..c6896b2de 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java +++ b/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java @@ -6,6 +6,7 @@ import com.sap.cds.reflect.CdsEntity; import com.sap.cds.sdm.caching.CacheConfig; import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.sdm.handler.applicationservice.helper.AttachmentsHandlerUtils; import com.sap.cds.sdm.model.AttachmentInfo; import com.sap.cds.services.persistence.PersistenceService; import java.io.IOException; @@ -32,11 +33,13 @@ private SDMUtils() { // Doesn't do anything } - public static Set isFileNameDuplicateInDrafts(List data, String composition) { + public static Set isFileNameDuplicateInDrafts( + List data, String composition, String targetEntity) { Set uniqueFilenames = new HashSet<>(); Set duplicateFilenames = new HashSet<>(); for (Map entity : data) { - List> attachments = (List>) entity.get(composition); + List> attachments = + AttachmentsHandlerUtils.fetchAttachments(targetEntity, entity, composition); if (attachments != null) { Iterator> iterator = attachments.iterator(); while (iterator.hasNext()) { diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java b/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java index 13d568e67..5a35fd04a 100644 --- a/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java @@ -530,7 +530,7 @@ public String renameAttachment( .build(); try (Response renameResponse = httpClient.newCall(request).execute()) { - if (renameResponse.code() != 200) { + if (!renameResponse.isSuccessful()) { System.out.println( "Rename Attachment failed in the " + facetName diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java b/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java index e961dc944..744b13183 100644 --- a/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java @@ -504,7 +504,7 @@ public String renameAttachment( .build(); try (Response renameResponse = httpClient.newCall(request).execute()) { - if (renameResponse.code() != 200) { + if (!renameResponse.isSuccessful()) { throw new IOException("Attachment was not renamed in section: " + facetName); } return "Renamed"; diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java b/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java index 0fcc83c38..3f97a8b7e 100644 --- a/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_MultipleFacet.java @@ -604,8 +604,8 @@ void testRenameEntitiesWithUnsupportedCharacter() { counter = -1; // Reset counter for the next check response = api.saveEntityDraft(appUrl, entityName, srvpath, entityID); String expected = - "[{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 sample/1234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," - + "{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 reference1/234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," + "[{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 reference1/234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," + + "{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 sample/1234\\n\\nRename the files and try again.\",\"numericSeverity\":3}," + "{\"code\":\"\",\"message\":\"Rename unsuccessful. The following filename(s) contain unsupported characters (/, \\\\). \\n\\n\\t\\u2022 footnote1/234\\n\\nRename the files and try again.\",\"numericSeverity\":3}]"; if (response.equals(expected)) { testStatus = true; @@ -673,7 +673,7 @@ void testRenameSingleDuplicate() { + "{\"code\":\"\",\"message\":\"The file(s) %s have been added multiple times. Please rename and try again.\",\"@Common.numericSeverity\":4}," + "{\"code\":\"\",\"message\":\"The file(s) %s have been added multiple times. Please rename and try again.\",\"@Common.numericSeverity\":4}" + "]}}", - name[0], name[1], name[2]); + name[1], name[0], name[2]); if (response.equals(expected)) { for (int i = 0; i < facet.length; i++) { // Attempt to rename again with a different name @@ -757,7 +757,7 @@ void testRenameEntitiesWithoutSDMRole() throws IOException { + // "\\n" + // - "\\t\\u2022 sample123\\n" + "\\t\\u2022 reference123\\n" + // "\\n" + // @@ -765,7 +765,7 @@ void testRenameEntitiesWithoutSDMRole() throws IOException { + // "\\n" + // - "\\t\\u2022 reference123\\n" + "\\t\\u2022 sample123\\n" + // "\\n" + // 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 03db40a13..dd0b4711f 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 @@ -9,8 +9,10 @@ import com.sap.cds.CdsData; import com.sap.cds.reflect.*; +import com.sap.cds.sdm.caching.CacheConfig; import com.sap.cds.sdm.handler.TokenHandler; import com.sap.cds.sdm.handler.applicationservice.SDMCreateAttachmentsHandler; +import com.sap.cds.sdm.handler.applicationservice.helper.AttachmentsHandlerUtils; import com.sap.cds.sdm.model.SDMCredentials; import com.sap.cds.sdm.persistence.DBQuery; import com.sap.cds.sdm.service.SDMService; @@ -24,7 +26,7 @@ import com.sap.cds.services.request.UserInfo; import java.io.IOException; import java.util.*; -import java.util.stream.Stream; +import org.ehcache.Cache; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -81,46 +83,58 @@ public void tearDown() { @Test public void testProcessBefore() throws IOException { - // Arrange the mock compositions scenario - List expectedCompositionNames = Arrays.asList("Name1", "Name2"); - - // Create a Stream of mocked CdsElement instances - Stream compositionsStream = Stream.of(cdsElement, cdsElement); - - when(context.getTarget().compositions()).thenReturn(compositionsStream); - when(cdsElement.getType()).thenReturn(cdsAssociationType); - when(cdsAssociationType.getTargetAspect()).thenReturn(Optional.of(targetAspect)); - when(cdsAssociationType.getTargetAspect().get().getQualifiedName()) - .thenReturn("sap.attachments.Attachments"); - when(cdsElement.getName()).thenReturn("Name1").thenReturn("Name2"); - - List dataList = new ArrayList<>(); - - // Act - handler.processBefore(context, dataList); - - // Assert that updateName was called with the compositions detected - for (String compositionName : expectedCompositionNames) { - verify(handler).updateName(context, dataList, compositionName); + try (MockedStatic attachmentsHandlerUtilsMocked = + mockStatic(AttachmentsHandlerUtils.class)) { + // Arrange the mock compositions scenario + Map expectedCompositionMapping = new HashMap<>(); + expectedCompositionMapping.put("Name1", "Name1"); + expectedCompositionMapping.put("Name2", "Name2"); + + // Mock AttachmentsHandlerUtils.getAttachmentPathMapping to return the expected mapping + attachmentsHandlerUtilsMocked + .when(() -> AttachmentsHandlerUtils.getAttachmentPathMapping(any(), any(), any())) + .thenReturn(expectedCompositionMapping); + + List dataList = new ArrayList<>(); + + // Act + handler.processBefore(context, dataList); + + // Assert that updateName was called with the compositions detected + for (Map.Entry entry : expectedCompositionMapping.entrySet()) { + verify(handler).updateName(context, dataList, entry.getKey(), entry.getValue()); + } } } @Test public void testUpdateNameWithDuplicateFilenames() throws IOException { - // Arrange - List data = new ArrayList<>(); - Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); - sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(duplicateFilenames); - - // Act - handler.updateName(context, data, "composition"); - - // Assert - verify(messages, times(1)) - .error( - "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Arrange + List data = new ArrayList<>(); + Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); + when(context.getMessages()).thenReturn(messages); + + // Mock the target entity + CdsEntity targetEntity = mock(CdsEntity.class); + when(targetEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getTarget()).thenReturn(targetEntity); + + sdmUtilsMockedStatic + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "compositionName", "TestEntity")) + .thenReturn(duplicateFilenames); + + // Act + handler.updateName(context, data, "compositionDefinition", "compositionName"); + + // Assert + verify(messages, times(1)) + .error( + "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); + } } @Test @@ -128,11 +142,11 @@ public void testUpdateNameWithEmptyData() throws IOException { // Arrange List data = new ArrayList<>(); sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "compositionName", "entity")) .thenReturn(Collections.emptySet()); // Act - handler.updateName(context, data, ""); + handler.updateName(context, data, "compositionDefinition", "compositionName"); // Assert verify(messages, never()).error(anyString()); @@ -141,32 +155,37 @@ public void testUpdateNameWithEmptyData() throws IOException { @Test public void testUpdateNameWithNoAttachments() throws IOException { - // Arrange - List data = new ArrayList<>(); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); - // Create an entity map without any attachments - Map entity = new HashMap<>(); + // Arrange + List data = new ArrayList<>(); - // Wrap the entity map in CdsData - CdsData cdsDataEntity = CdsData.create(entity); + // Create an entity map without any attachments + Map entity = new HashMap<>(); - // Add the CdsData entity to the data list - data.add(cdsDataEntity); + // Wrap the entity map in CdsData + CdsData cdsDataEntity = CdsData.create(entity); - // Mock utility methods - sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(Collections.emptySet()); + // Add the CdsData entity to the data list + data.add(cdsDataEntity); - // Act - handler.updateName(context, data, ""); + // Mock utility methods + sdmUtilsMockedStatic + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "compositionName", "entity")) + .thenReturn(Collections.emptySet()); - // Assert that no updateAttachments calls were made, as there are no attachments - verify(sdmService, never()).updateAttachments(any(), any(), any(), any(), anyBoolean()); + // Act + handler.updateName(context, data, "compositionDefinition", "compositionName"); - // Assert that no error or warning messages were logged - verify(messages, never()).error(anyString()); - verify(messages, never()).warn(anyString()); + // Assert that no updateAttachments calls were made, as there are no attachments + verify(sdmService, never()).updateAttachments(any(), any(), any(), any(), anyBoolean()); + + // Assert that no error or warning messages were logged + verify(messages, never()).error(anyString()); + verify(messages, never()).warn(anyString()); + } } // @Test @@ -383,88 +402,110 @@ public void testUpdateNameWithNoAttachments() throws IOException { // } @Test public void testUpdateNameWithEmptyFilename() throws IOException { - List data = new ArrayList<>(); - Map entity = new HashMap<>(); - List> attachments = new ArrayList<>(); - - Map attachment = new HashMap<>(); - attachment.put("ID", "test-id"); - attachment.put("fileName", null); // Empty filename - attachment.put("objectId", "test-object-id"); - attachments.add(attachment); - - // entity.put("attachments", attachments); - entity.put("composition", attachments); - - CdsData cdsDataEntity = CdsData.create(entity); // Wrap entity in CdsData - data.add(cdsDataEntity); // Add to data - - // Mock duplicate file name - sdmUtilsMockedStatic - .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(new HashSet<>()); - - // Mock attachment entity - CdsEntity attachmentDraftEntity = mock(CdsEntity.class); - when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); - when(context.getTarget()).thenReturn(attachmentDraftEntity); - when(context.getModel()).thenReturn(model); - - // Mock findEntity to return an optional containing attachmentDraftEntity - when(model.findEntity("some.qualified.Name" + "." + "composition")) - .thenReturn(Optional.of(attachmentDraftEntity)); - UserInfo userInfo = Mockito.mock(UserInfo.class); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.isSystemUser()).thenReturn(false); - // Mock authentication - when(context.getMessages()).thenReturn(messages); - when(context.getAuthenticationInfo()).thenReturn(authInfo); - when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); - when(jwtTokenInfo.getToken()).thenReturn("testJwtToken"); - - // Mock getObject - when(sdmService.getObject("test-object-id", mockCredentials, false)) - .thenReturn("fileInSDM.txt"); - - // Mock getSecondaryTypeProperties - Map secondaryTypeProperties = new HashMap<>(); - Map updatedSecondaryProperties = new HashMap<>(); - sdmUtilsMockedStatic - .when( - () -> - SDMUtils.getSecondaryTypeProperties(Optional.of(attachmentDraftEntity), attachment)) - .thenReturn(secondaryTypeProperties); - sdmUtilsMockedStatic - .when( - () -> - SDMUtils.getUpdatedSecondaryProperties( - Optional.of(attachmentDraftEntity), - attachment, - persistenceService, - secondaryTypeProperties, - updatedSecondaryProperties)) - .thenReturn(new HashMap<>()); - - // Mock restricted character - sdmUtilsMockedStatic - .when(() -> SDMUtils.isRestrictedCharactersInName("fileNameInRequest")) - .thenReturn(false); - - when(dbQuery.getAttachmentForID(attachmentDraftEntity, persistenceService, "test-id")) - .thenReturn(null); - - // When getPropertiesForID is called - when(dbQuery.getPropertiesForID( - attachmentDraftEntity, persistenceService, "test-id", secondaryTypeProperties)) - .thenReturn(updatedSecondaryProperties); - - // Act & Assert - ServiceException exception = - assertThrows( - ServiceException.class, () -> handler.updateName(context, data, "composition")); - - // Assert that the correct exception message is returned - assertEquals("Filename cannot be empty", exception.getMessage()); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + + Map attachment = new HashMap<>(); + attachment.put("ID", "test-id"); + attachment.put("fileName", null); // Empty filename + attachment.put("objectId", "test-object-id"); + attachments.add(attachment); + + // entity.put("attachments", attachments); + entity.put("composition", attachments); + + CdsData cdsDataEntity = CdsData.create(entity); // Wrap entity in CdsData + data.add(cdsDataEntity); // Add to data + + // Mock duplicate file name + sdmUtilsMockedStatic + .when( + () -> + SDMUtils.isFileNameDuplicateInDrafts( + data, "compositionName", "some.qualified.Name")) + .thenReturn(new HashSet<>()); + + // Mock AttachmentsHandlerUtils.fetchAttachments to return the attachment with null filename + try (MockedStatic attachmentsHandlerUtilsMocked = + mockStatic(AttachmentsHandlerUtils.class)) { + attachmentsHandlerUtilsMocked + .when( + () -> + AttachmentsHandlerUtils.fetchAttachments( + "some.qualified.Name", entity, "compositionName")) + .thenReturn(attachments); + + // Mock attachment entity + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + + // Mock findEntity to return an optional containing attachmentDraftEntity + when(model.findEntity("compositionDefinition")) + .thenReturn(Optional.of(attachmentDraftEntity)); + UserInfo userInfo = Mockito.mock(UserInfo.class); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.isSystemUser()).thenReturn(false); + // Mock authentication + when(context.getMessages()).thenReturn(messages); + when(context.getAuthenticationInfo()).thenReturn(authInfo); + when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); + when(jwtTokenInfo.getToken()).thenReturn("testJwtToken"); + + // Mock getObject + when(sdmService.getObject("test-object-id", mockCredentials, false)) + .thenReturn("fileInSDM.txt"); + + // Mock getSecondaryTypeProperties + Map secondaryTypeProperties = new HashMap<>(); + Map updatedSecondaryProperties = new HashMap<>(); + sdmUtilsMockedStatic + .when( + () -> + SDMUtils.getSecondaryTypeProperties( + Optional.of(attachmentDraftEntity), attachment)) + .thenReturn(secondaryTypeProperties); + sdmUtilsMockedStatic + .when( + () -> + SDMUtils.getUpdatedSecondaryProperties( + Optional.of(attachmentDraftEntity), + attachment, + persistenceService, + secondaryTypeProperties, + updatedSecondaryProperties)) + .thenReturn(new HashMap<>()); + + // Mock restricted character + sdmUtilsMockedStatic + .when(() -> SDMUtils.isRestrictedCharactersInName("fileNameInRequest")) + .thenReturn(false); + + when(dbQuery.getAttachmentForID(attachmentDraftEntity, persistenceService, "test-id")) + .thenReturn(null); + + // When getPropertiesForID is called + when(dbQuery.getPropertiesForID( + attachmentDraftEntity, persistenceService, "test-id", secondaryTypeProperties)) + .thenReturn(updatedSecondaryProperties); + + // Act & Assert + ServiceException exception = + assertThrows( + ServiceException.class, + () -> + handler.updateName(context, data, "compositionDefinition", "compositionName")); + + // Assert that the correct exception message is returned + assertEquals("Filename cannot be empty", exception.getMessage()); + } // Close AttachmentsHandlerUtils mock + } } // @Test 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 2c9cf5259..30a550235 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 @@ -9,9 +9,11 @@ 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.SDMUpdateAttachmentsHandler; +import com.sap.cds.sdm.handler.applicationservice.helper.AttachmentsHandlerUtils; import com.sap.cds.sdm.model.CmisDocument; import com.sap.cds.sdm.model.SDMCredentials; import com.sap.cds.sdm.persistence.DBQuery; @@ -26,7 +28,7 @@ import com.sap.cds.services.request.UserInfo; import java.io.IOException; import java.util.*; -import java.util.stream.Stream; +import org.ehcache.Cache; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.*; @@ -74,54 +76,69 @@ public void tearDown() { @Test public void testProcessBefore() throws IOException { - // Arrange - List expectedCompositionNames = Arrays.asList("Name1", "Name2"); - - // Simulate a stream of CdsElement instances returned from the mock target's compositions - Stream compositionsStream = Stream.of(cdsElement, cdsElement); + try (MockedStatic attachmentsHandlerUtilsMocked = + mockStatic(AttachmentsHandlerUtils.class); + MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Arrange the mock compositions scenario + Map expectedCompositionMapping = new HashMap<>(); + expectedCompositionMapping.put("Name1", "Name1"); + expectedCompositionMapping.put("Name2", "Name2"); + + // Mock context.getTarget() and context.getModel() + when(context.getTarget()).thenReturn(targetEntity); + when(targetEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getModel()).thenReturn(model); + when(model.findEntity(anyString())).thenReturn(Optional.of(targetEntity)); + + // Mock AttachmentsHandlerUtils.getAttachmentPathMapping to return the expected mapping + attachmentsHandlerUtilsMocked + .when(() -> AttachmentsHandlerUtils.getAttachmentPathMapping(any(), any(), any())) + .thenReturn(expectedCompositionMapping); + + List dataList = new ArrayList<>(); + + // Act + handler.processBefore(context, dataList); + + // Assert that updateName was called with the compositions detected + for (Map.Entry entry : expectedCompositionMapping.entrySet()) { + verify(handler).updateName(context, dataList, entry.getKey(), entry.getValue()); + } + } + } - // mock the target and model of the context - when(context.getTarget()).thenReturn(targetEntity); - when(targetEntity.compositions()).thenReturn(compositionsStream); - when(context.getModel()).thenReturn(model); + @Test + public void testRenameWithDuplicateFilenames() throws IOException { + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); - // Mock findEntity to return an optional containing attachmentDraftEntity - when(model.findEntity(anyString())).thenReturn(Optional.of(targetEntity)); + List data = new ArrayList<>(); + Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); + when(context.getMessages()).thenReturn(messages); - // Mock the elements and their associations - when(cdsElement.getType()).thenReturn(cdsAssociationType); - when(cdsAssociationType.getTargetAspect()).thenReturn(Optional.of(targetAspect)); - when(targetAspect.getQualifiedName()).thenReturn("sap.attachments.Attachments"); - when(cdsElement.getName()).thenReturn("Name1").thenReturn("Name2"); + // Mock the target entity + CdsEntity targetEntity = mock(CdsEntity.class); + when(targetEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getTarget()).thenReturn(targetEntity); - List dataList = new ArrayList<>(); + sdmUtilsMockedStatic = mockStatic(SDMUtils.class); + sdmUtilsMockedStatic + .when(() -> isFileNameDuplicateInDrafts(data, "compositionName", "TestEntity")) + .thenReturn(duplicateFilenames); - // Act - handler.processBefore(context, dataList); + handler.updateName(context, data, "compositionDefinition", "compositionName"); - // Assert that updateName was called with the compositions detected - for (String compositionName : expectedCompositionNames) { - verify(handler).updateName(context, dataList, compositionName); + verify(messages, times(1)) + .error( + "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); } } - @Test - public void testRenameWithDuplicateFilenames() throws IOException { - List data = new ArrayList<>(); - Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); - when(context.getMessages()).thenReturn(messages); - sdmUtilsMockedStatic = mockStatic(SDMUtils.class); - sdmUtilsMockedStatic - .when(() -> isFileNameDuplicateInDrafts(data, "composition")) - .thenReturn(duplicateFilenames); - - handler.updateName(context, data, "composition"); - - verify(messages, times(1)) - .error( - "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); - } - // @Test // public void testRenameWithUniqueFilenames() throws IOException { // List data = prepareMockAttachmentData("file1.txt"); @@ -212,62 +229,125 @@ public void testRenameWithDuplicateFilenames() throws IOException { @Test public void testRenameWithNoSDMRoles() throws IOException { - // Mock the data structure to simulate the attachments - List data = new ArrayList<>(); - Map entity = new HashMap<>(); - List> attachments = new ArrayList<>(); - Map attachment = spy(new HashMap<>()); - Map secondaryProperties = new HashMap<>(); - Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); - secondaryProperties.put("filename", "file1.txt"); - - CmisDocument document = new CmisDocument(); - document.setFileName("file1.txt"); - - attachment.put("fileName", "file1.txt"); - attachment.put("url", "objectId"); - attachment.put("ID", "test-id"); - attachments.add(attachment); - - entity.put("attachments", attachments); - CdsData mockCdsData = mock(CdsData.class); - when(mockCdsData.get("composition")).thenReturn(attachments); - data.add(mockCdsData); - - CdsEntity attachmentDraftEntity = mock(CdsEntity.class); - when(context.getTarget()).thenReturn(attachmentDraftEntity); - when(context.getModel()).thenReturn(model); - when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); - when(model.findEntity("some.qualified.Name.composition")) - .thenReturn(Optional.of(attachmentDraftEntity)); - when(context.getMessages()).thenReturn(messages); - UserInfo userInfo = Mockito.mock(UserInfo.class); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.isSystemUser()).thenReturn(false); - when(tokenHandler.getSDMCredentials()).thenReturn(mockCredentials); - when(dbQuery.getAttachmentForID( - any(CdsEntity.class), any(PersistenceService.class), anyString())) - .thenReturn("file123.txt"); - - when(sdmService.updateAttachments( - mockCredentials, - document, - secondaryProperties, - secondaryPropertiesWithInvalidDefinitions, - false)) - .thenReturn(403); // Forbidden + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class); + MockedStatic attachmentsMockStatic = + mockStatic(AttachmentsHandlerUtils.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Mock the data structure to simulate the attachments + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + Map attachment = spy(new HashMap<>()); + Map secondaryProperties = new HashMap<>(); + Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); + secondaryProperties.put("filename", "file1.txt"); - // Call the method - handler.updateName(context, data, "composition"); + CmisDocument document = new CmisDocument(); + document.setFileName("file1.txt"); - // Capture and assert the warning message - ArgumentCaptor warningCaptor = ArgumentCaptor.forClass(String.class); - verify(messages).warn(warningCaptor.capture()); - String warningMessage = warningCaptor.getValue(); + attachment.put("fileName", "file1.txt"); + attachment.put("url", "objectId"); + attachment.put("ID", "test-id"); + attachments.add(attachment); - String expectedMessage = - SDMConstants.noSDMRolesMessage(Collections.singletonList("file123.txt"), "update"); - assertEquals(expectedMessage, warningMessage); + entity.put("compositionName", attachments); + CdsData mockCdsData = mock(CdsData.class); + data.add(mockCdsData); + + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + when(model.findEntity("compositionDefinition")) + .thenReturn(Optional.of(attachmentDraftEntity)); + when(context.getMessages()).thenReturn(messages); + UserInfo userInfo = Mockito.mock(UserInfo.class); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.isSystemUser()).thenReturn(false); + when(tokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + when(dbQuery.getAttachmentForID( + any(CdsEntity.class), any(PersistenceService.class), anyString())) + .thenReturn("file123.txt"); + + when(sdmService.updateAttachments( + mockCredentials, + document, + secondaryProperties, + secondaryPropertiesWithInvalidDefinitions, + false)) + .thenReturn(403); // Forbidden + + // Mock AttachmentsHandlerUtils.fetchAttachments + attachmentsMockStatic + .when( + () -> + AttachmentsHandlerUtils.fetchAttachments( + anyString(), any(Map.class), eq("compositionName"))) + .thenReturn(attachments); + + // Mock SDMUtils methods + try (MockedStatic sdmUtilsMock = mockStatic(SDMUtils.class)) { + sdmUtilsMock + .when( + () -> + SDMUtils.isFileNameDuplicateInDrafts( + any(List.class), eq("compositionName"), anyString())) + .thenReturn(Collections.emptySet()); + + sdmUtilsMock + .when(() -> SDMUtils.getPropertyTitles(any(Optional.class), any(Map.class))) + .thenReturn(Collections.emptyMap()); + + sdmUtilsMock + .when( + () -> + SDMUtils.getSecondaryPropertiesWithInvalidDefinition( + any(Optional.class), any(Map.class))) + .thenReturn(Collections.emptyMap()); + + sdmUtilsMock + .when(() -> SDMUtils.getSecondaryTypeProperties(any(Optional.class), any(Map.class))) + .thenReturn(Collections.emptyMap()); + + sdmUtilsMock + .when( + () -> + SDMUtils.getUpdatedSecondaryProperties( + any(Optional.class), + any(Map.class), + any(PersistenceService.class), + any(Map.class), + any(Map.class))) + .thenReturn(secondaryProperties); + + sdmUtilsMock + .when(() -> SDMUtils.isRestrictedCharactersInName(anyString())) + .thenReturn(false); + + // Call the method + handler.updateName(context, data, "compositionDefinition", "compositionName"); + + // Capture and assert the warning message + ArgumentCaptor warningCaptor = ArgumentCaptor.forClass(String.class); + verify(messages).warn(warningCaptor.capture()); + String warningMessage = warningCaptor.getValue(); + + String expectedMessage = + SDMConstants.noSDMRolesMessage(Collections.singletonList("file123.txt"), "update"); + assertEquals(expectedMessage, warningMessage); + } + + // Capture and assert the warning message + ArgumentCaptor warningCaptor = ArgumentCaptor.forClass(String.class); + verify(messages).warn(warningCaptor.capture()); + String warningMessage = warningCaptor.getValue(); + + String expectedMessage = + SDMConstants.noSDMRolesMessage(Collections.singletonList("file123.txt"), "update"); + assertEquals(expectedMessage, warningMessage); + } } // @Test @@ -409,35 +489,41 @@ public void testRenameWithoutFileInSDM() throws IOException { @Test public void testRenameWithNoAttachments() throws IOException { - // Arrange - List data = new ArrayList<>(); - CdsEntity attachmentDraftEntity = mock(CdsEntity.class); - Map secondaryProperties = new HashMap<>(); - Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); - CmisDocument document = new CmisDocument(); - when(context.getTarget()).thenReturn(attachmentDraftEntity); - when(context.getModel()).thenReturn(model); - - when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); - - String expectedEntityName = "some.qualified.Name.attachments"; - when(model.findEntity(expectedEntityName)).thenReturn(Optional.of(attachmentDraftEntity)); - - CdsData mockCdsData = mock(CdsData.class); - when(mockCdsData.get("attachments")).thenReturn(null); - data.add(mockCdsData); - - // Act - handler.updateName(context, data, "attachments"); - - // Assert - verify(sdmService, never()) - .updateAttachments( - eq(mockCredentials), - eq(document), - eq(secondaryProperties), - eq(secondaryPropertiesWithInvalidDefinitions), - eq(false)); + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryPropertiesCache).thenReturn(mockCache); + + // Arrange + List data = new ArrayList<>(); + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + Map secondaryProperties = new HashMap<>(); + Map secondaryPropertiesWithInvalidDefinitions = new HashMap<>(); + CmisDocument document = new CmisDocument(); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + + // Mock the correct entity name that the handler will look for + when(model.findEntity("compositionDefinition")) + .thenReturn(Optional.of(attachmentDraftEntity)); + + Map entity = new HashMap<>(); + CdsData cdsDataEntity = CdsData.create(entity); + data.add(cdsDataEntity); + + // Act + handler.updateName(context, data, "compositionDefinition", "compositionName"); + + // Assert + verify(sdmService, never()) + .updateAttachments( + eq(mockCredentials), + eq(document), + eq(secondaryProperties), + eq(secondaryPropertiesWithInvalidDefinitions), + eq(false)); + } } // @Test diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java index 0a3f98001..7ef6443d5 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java @@ -157,6 +157,9 @@ void testCreate_shouldCreateLink() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -249,6 +252,9 @@ void testCreate_ShouldThrowSpecifiedExceptionWhenMaxCountReached() throws IOExce when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -307,6 +313,9 @@ void testCreate_ShouldThrowDefaultExceptionWhenMaxCountReached() throws IOExcept when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -365,6 +374,9 @@ void testCreate_ShouldThrowExceptionWhenRestrictedCharacterInLinkName() throws I when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("test/URL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -425,6 +437,9 @@ void testCreate_ThrowsServiceExceptionOnDuplicateFile() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -486,6 +501,9 @@ void testCreate_ThrowsServiceException_WhenCreateDocumentThrowsException() throw when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("testURL"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -554,6 +572,9 @@ void testCreate_ThrowsServiceExceptionOnDuplicateStatus() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -623,6 +644,9 @@ void testCreate_ThrowsServiceExceptionOnFailStatus() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); @@ -691,6 +715,9 @@ void testCreate_ThrowsServiceExceptionOnUnauthorizedStatus() throws IOException when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); + when(cqnSelect.toString()) + .thenReturn( + "{\"SELECT\":{\"from\":{\"ref\":[\"entity1\",{\"where\":[{\"ref\":[\"ID\"]},\"=\",{\"val\":\"123\"}]},\"entity2\"]}}}"); when(mockContext.get("cqn")).thenReturn(cqnSelect); when(mockContext.get("name")).thenReturn("duplicateFile.txt"); when(mockContext.get("url")).thenReturn("http://test-url"); diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java index fa7f35387..dedb2f5ec 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java @@ -71,7 +71,6 @@ private void setUp() { @Test public void testIsFileNameDuplicateInDrafts() { List data = new ArrayList<>(); - CdsData mockCdsData = mock(CdsData.class); Map entity = new HashMap<>(); List> attachments = new ArrayList<>(); Map attachment1 = new HashMap<>(); @@ -82,11 +81,15 @@ public void testIsFileNameDuplicateInDrafts() { attachment2.put("repositoryId", "repo1"); attachments.add(attachment1); attachments.add(attachment2); - entity.put("attachments", attachments); - when(mockCdsData.get("attachments")).thenReturn(attachments); // Correctly mock get method - data.add(mockCdsData); - Set duplicateFilenames = SDMUtils.isFileNameDuplicateInDrafts(data, "attachments"); + // Create the nested structure that fetchAttachments expects + Map entityData = new HashMap<>(); + entityData.put("attachmentCompositionName", attachments); + entity.put("entity", entityData); + data.add(CdsData.create(entity)); + + Set duplicateFilenames = + SDMUtils.isFileNameDuplicateInDrafts(data, "attachmentCompositionName", "entity"); assertTrue(duplicateFilenames.contains("file1.txt")); } @@ -640,7 +643,6 @@ void testElementWithoutAnnotation() { void testElementWithAnnotation() { CdsEntity entity = mock(CdsEntity.class); CdsElement element = mock(CdsElement.class); - @SuppressWarnings("unchecked") CdsAnnotation annotation = mock(CdsAnnotation.class); when(annotation.getValue()).thenReturn("name");