Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
The format is based on [Keep a Changelog](http://keepachangelog.com/).

## Version 1.10.2

### Fixed
- Fix `element does not exist` error when querying attachments on non-draft entities. Queries in `getAttachmentsForUPID`, `getAttachmentsForUPIDAndRepository`, and `getAttachmentsForFolder` now conditionally include the `IsActiveEntity` column only when the field is present on the attachment entity.
- Fix `Invalid CQN: Element 'SDM_READONLY_CONTEXT.uploadStatus' does not exist` error during `draftActivate` on deeply nested entities (5+ levels deep or entities with many compositions). The internal `SDM_READONLY_CONTEXT` wrapper key used to preserve `uploadStatus` across handler phases was being removed using plain `Map.values()` traversal, which cannot traverse all composition data in deeply nested CDS structures. The removal now uses `CdsDataProcessor` (the same model-driven traversal used when setting the key), ensuring the wrapper is correctly cleaned up regardless of entity depth or breadth.
- Fix `NoSuchElementException` in `revertLinksForComposition` by safely unwrapping `Optional` when looking up draft and active entities in the CDS model
- Skip credential fetch and SDM revert call when no draft links are found, avoiding unnecessary processing

## Version 1.10.1

### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,28 @@ public void updateActiveEntitySdmMetadata(CdsCreateEventContext _context) {
}

private void handleUpdateActiveEntitySdmMetadata() {
logger.debug(
"[CREATE] handleUpdateActiveEntitySdmMetadata: checking ThreadLocal for SDM metadata");
Map<String, Object> metadata = SDMAttachmentsServiceHandler.SDM_METADATA_THREADLOCAL.get();
if (metadata == null) {
logger.debug(
"[CREATE] handleUpdateActiveEntitySdmMetadata: no ThreadLocal metadata found, skipping");
return;
}
try {
SDMAttachmentsServiceHandler.SDM_METADATA_THREADLOCAL.remove();
logger.debug(
"[CREATE] handleUpdateActiveEntitySdmMetadata: ThreadLocal metadata keys: {}",
metadata.keySet());
com.sap.cds.reflect.CdsEntity attachmentEntity =
(com.sap.cds.reflect.CdsEntity) metadata.get("attachmentEntity");
if (attachmentEntity == null) {
logger.warn("No attachmentEntity in ThreadLocal metadata, skipping post-INSERT update");
return;
}
logger.debug(
"[CREATE] handleUpdateActiveEntitySdmMetadata: attachmentEntity={}",
attachmentEntity.getQualifiedName());
CmisDocument cmisDocument = new CmisDocument();
cmisDocument.setAttachmentId((String) metadata.get("attachmentId"));
cmisDocument.setObjectId((String) metadata.get("objectId"));
Expand All @@ -109,7 +119,7 @@ public void processBefore(CdsCreateEventContext context, List<CdsData> data) thr
logger.info(
"START: Process attachments before persistence for entity: {}",
context.getTarget().getQualifiedName());
logger.debug("Number of entities to process: {}", data.size());
logger.info("Number of entities to process: {}", data.size());

for (CdsData entityData : data) {
Map<String, Map<String, String>> attachmentCompositionDetails =
Expand All @@ -119,7 +129,7 @@ public void processBefore(CdsCreateEventContext context, List<CdsData> data) thr
persistenceService,
context.getTarget().getQualifiedName(),
entityData);
logger.debug("Attachment compositions present: {}", attachmentCompositionDetails.keySet());
logger.info("Attachment compositions found: {}", attachmentCompositionDetails.keySet());
updateName(context, data, attachmentCompositionDetails);
// Remove uploadStatus from attachment data to prevent validation errors
cleanupReadonlyContextsForAttachments(context, entityData, attachmentCompositionDetails);
Expand Down Expand Up @@ -151,32 +161,45 @@ public void processAfter(CdsCreateEventContext context, List<CdsData> data) {
Optional<CdsEntity> attachmentEntity =
context.getModel().findEntity(attachmentCompositionDefinition);

if (attachmentEntity.isPresent()) {
String targetEntity = context.getTarget().getQualifiedName();
List<Map<String, Object>> attachments =
AttachmentsHandlerUtils.fetchAttachments(
targetEntity, entityData, attachmentCompositionName);
if (!attachmentEntity.isPresent()) {
logger.warn(
"[SDM] CREATE: Attachment entity '{}' not found in CDS model — skipping uploadStatus persistence for composition '{}'",
attachmentCompositionDefinition,
attachmentCompositionName);
continue;
}

if (attachments != null) {
logger.debug(
"Processing {} attachments for composition: {}",
attachments.size(),
attachmentCompositionName);
for (Map<String, Object> attachment : attachments) {
String id = (String) attachment.get("ID");
String uploadStatus = (String) attachment.get("uploadStatus");
if (id != null) {
CmisDocument cmisDocument = new CmisDocument();
cmisDocument.setAttachmentId(id);
cmisDocument.setUploadStatus(uploadStatus);
logger.debug("Saving uploadStatus: {} for attachment ID: {}", uploadStatus, id);
// Update uploadStatus to Success in database if it was InProgress
dbQuery.saveUploadStatusToAttachment(
attachmentEntity.get(), persistenceService, cmisDocument);
totalProcessed++;
}
String targetEntity = context.getTarget().getQualifiedName();
List<Map<String, Object>> attachments =
AttachmentsHandlerUtils.fetchAttachments(
targetEntity, entityData, attachmentCompositionName);

if (attachments != null && !attachments.isEmpty()) {
logger.info(
"[SDM] CREATE: Persisting uploadStatus for {} attachment(s) in composition '{}'",
attachments.size(),
attachmentCompositionName);
for (Map<String, Object> attachment : attachments) {
String id = (String) attachment.get("ID");
String uploadStatus = (String) attachment.get("uploadStatus");
if (id != null) {
logger.debug("Saving uploadStatus '{}' for attachment ID: {}", uploadStatus, id);
CmisDocument cmisDocument = new CmisDocument();
cmisDocument.setAttachmentId(id);
cmisDocument.setUploadStatus(uploadStatus);
dbQuery.saveUploadStatusToAttachment(
attachmentEntity.get(), persistenceService, cmisDocument);
totalProcessed++;
} else {
logger.warn(
"[SDM] CREATE: Attachment in composition '{}' has no ID — skipping uploadStatus persistence",
attachmentCompositionName);
}
}
} else {
logger.debug(
"No attachments in payload for composition '{}' during post-processing",
attachmentCompositionName);
}
}
}
Expand All @@ -188,9 +211,12 @@ public void processAfter(CdsCreateEventContext context, List<CdsData> data) {
public void preserveUploadStatus(CdsCreateEventContext context, List<CdsData> data) {
// Preserve uploadStatus before CDS removes readonly fields
logger.debug(
"Preserving readonly fields (uploadStatus) for entity: {} before CDS capability check",
context.getTarget().getQualifiedName());
"[CREATE] preserveUploadStatus: entity={} dataSize={}",
context.getTarget().getQualifiedName(),
data.size());
SDMUtils.preserveReadonlyFields(context.getTarget(), data);
logger.debug(
"[CREATE] preserveUploadStatus: SDM_READONLY_CONTEXT set on attachment maps via CdsDataProcessor");
}

public void updateName(
Expand Down Expand Up @@ -218,9 +244,18 @@ public void updateName(

Optional<CdsEntity> attachmentEntity =
context.getModel().findEntity(attachmentCompositionDefinition);
logger.debug(
"[CREATE] updateName: processing composition={} entityFound={}",
attachmentCompositionName,
attachmentEntity.isPresent());
isError =
AttachmentsHandlerUtils.validateFileNames(
context, data, attachmentCompositionName, contextInfo, attachmentEntity);
if (isError) {
logger.debug(
"[CREATE] updateName: filename validation failed for composition={}, skipping SDM update",
attachmentCompositionName);
}
if (!isError) {
List<String> fileNameWithRestrictedCharacters = new ArrayList<>();
List<String> duplicateFileNameList = new ArrayList<>();
Expand Down Expand Up @@ -671,7 +706,42 @@ private void cleanupReadonlyContextsForAttachments(
}
}
} else {
logger.debug("No attachments found for composition: {}", attachmentCompositionName);
logger.debug(
"[SDM] CREATE: fetchAttachments returned no results for composition '{}' on entity '{}'. "
+ "This may indicate a deeply nested composition whose property name does not match the entity name. "
+ "Fallback recursive cleanup will handle SDM_READONLY_CONTEXT removal.",
attachmentCompositionName,
targetEntity);
}
}
// Use CdsDataProcessor to mirror the exact traversal path used by preserveReadonlyFields.
// This handles cases where CdsData stores composition data internally (e.g. during
// draftActivate) in a way that plain Map.values() iteration cannot reach.
SDMUtils.removeReadonlyFields(context.getTarget(), List.of(CdsData.create(entityData)));
// Plain-map recursive fallback as secondary safety net for any remaining entries.
removeReadonlyContextRecursively(entityData);
}

@SuppressWarnings("unchecked")
private void removeReadonlyContextRecursively(Map<String, Object> data) {
if (data == null) {
return;
}
if (data.containsKey(SDM_READONLY_CONTEXT)) {
logger.warn(
"[SDM] CREATE: Fallback removed SDM_READONLY_CONTEXT from map with keys: {}. "
+ "This entry was not cleaned up by the composition-based path — "
+ "likely a deeply nested or mismatched composition name.",
data.keySet());
data.remove(SDM_READONLY_CONTEXT);
}
for (Object value : data.values()) {
if (value instanceof List) {
for (Object item : (List<?>) value) {
if (item instanceof Map) {
removeReadonlyContextRecursively((Map<String, Object>) item);
}
}
}
}
}
Expand Down
Loading
Loading