Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ private SDMErrorMessages() {
public static final String INVALID_SECONDARY_PROPERTIES_FOR_MOVE_SUFFIX =
". Attachment rolled back to source.";
public static final String SDM_MOVE_OPERATION_FAILED = "SDM move operation failed";
public static final String FAILED_TO_COPY_ATTACHMENTS_PREFIX =
"Failed to copy the following attachments:\n";
public static final String INVALID_SECONDARY_PROPERTIES_FOR_COPY_PREFIX =
"Invalid secondary properties detected: ";
public static final String INVALID_SECONDARY_PROPERTIES_FOR_COPY_SUFFIX =
". Attachment not copied.";
public static final String FAILED_TO_ACCESS_ERROR_KEY_FIELDS =
"Failed to access SDM error key fields";
public static final String FAILED_TO_ACCESS_ERROR_MESSAGES_FIELDS =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ private SDMUIErrorKeys() {}
"SDM.invalidSecondaryPropertiesForMovePrefix";
public static final String INVALID_SECONDARY_PROPERTIES_FOR_MOVE_SUFFIX_KEY =
"SDM.invalidSecondaryPropertiesForMoveSuffix";
public static final String FAILED_TO_COPY_ATTACHMENTS_PREFIX_KEY =
"SDM.failedToCopyAttachmentsPrefix";
public static final String INVALID_SECONDARY_PROPERTIES_FOR_COPY_PREFIX_KEY =
"SDM.invalidSecondaryPropertiesForCopyPrefix";
public static final String INVALID_SECONDARY_PROPERTIES_FOR_COPY_SUFFIX_KEY =
"SDM.invalidSecondaryPropertiesForCopySuffix";
public static final String MAX_COUNT_ERROR_MESSAGE_KEY = "SDM.maxCountErrorMessage";

public static Map<String, Object> getAllUIErrorKeys() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,42 @@ public void copyAttachments(AttachmentCopyEventContext context) throws IOExcepti
Boolean isSystemUser = context.getSystemUser();

SDMCredentials sdmCredentials = tokenHandler.getSDMCredentials();

List<String> objectIds = context.getObjectIds();

if (!customPropertiesInSDM.isEmpty()) {
List<Map<String, String>> copyFailures =
findAttachmentsWithInvalidSecondaryProperties(
objectIds, customPropertiesInSDM, repositoryId, sdmCredentials, isSystemUser);
if (!copyFailures.isEmpty()) {
buildAndWarnCopyFailures(copyFailures, context);
// Remove invalid objectIds and proceed with valid ones
Set<String> invalidObjectIds =
copyFailures.stream()
.map(failure -> failure.get(OBJECT_ID_KEY))
.collect(Collectors.toSet());
objectIds =
objectIds.stream()
.filter(id -> !invalidObjectIds.contains(id))
.collect(Collectors.toList());
if (objectIds.isEmpty()) {
context.setCompleted();
logger.debug("END: Copy attachments event - all attachments have invalid properties");
return;
}
logger.info(
"Proceeding with {} valid attachments after filtering out {} invalid ones",
objectIds.size(),
invalidObjectIds.size());
}
}

// Check if folder exists before trying to create it
boolean folderExists =
sdmService.getFolderIdByPath(folderName, repositoryId, sdmCredentials, isSystemUser)
!= null;
String folderId = ensureFolderExists(folderName, repositoryId, sdmCredentials, isSystemUser);

List<String> objectIds = context.getObjectIds();

CopyAttachmentsRequest request =
CopyAttachmentsRequest.builder()
.context(context)
Expand Down Expand Up @@ -178,6 +206,97 @@ public void copyAttachments(AttachmentCopyEventContext context) throws IOExcepti
logger.debug("END: Copy attachments event");
}

/**
* Checks source attachments for invalid secondary properties before copy. Returns a list of
* failures if any attachment has invalid properties; returns empty list if all are valid
*/
private List<Map<String, String>> findAttachmentsWithInvalidSecondaryProperties(
List<String> objectIds,
Set<String> customPropertiesInSDM,
String repositoryId,
SDMCredentials sdmCredentials,
Boolean isSystemUser)
throws IOException {
List<String> secondaryTypes =
sdmService.getSecondaryTypes(repositoryId, sdmCredentials, isSystemUser);
List<String> validSecondaryProperties =
sdmService.getValidSecondaryProperties(
secondaryTypes, sdmCredentials, repositoryId, isSystemUser);

List<Map<String, String>> failures = new ArrayList<>();
for (String objectId : objectIds) {
List<String> invalidProperties =
getInvalidPropertiesForObject(
objectId,
customPropertiesInSDM,
validSecondaryProperties,
sdmCredentials,
isSystemUser);
if (!invalidProperties.isEmpty()) {
Map<String, String> failure = new HashMap<>();
failure.put(OBJECT_ID_KEY, objectId);
failure.put(
FAILURE_REASON_KEY,
SDMUtils.getErrorMessage("INVALID_SECONDARY_PROPERTIES_FOR_COPY_PREFIX")
+ String.join(", ", invalidProperties)
+ SDMUtils.getErrorMessage("INVALID_SECONDARY_PROPERTIES_FOR_COPY_SUFFIX"));
failures.add(failure);
}
}
return failures;
}

/**
* Gets the list of invalid secondary properties for a single object from SDM. Returns empty list
* if all properties are valid or if metadata cannot be fetched
*/
private List<String> getInvalidPropertiesForObject(
String objectId,
Set<String> customPropertiesInSDM,
List<String> validSecondaryProperties,
SDMCredentials sdmCredentials,
Boolean isSystemUser) {
try {
JSONObject sdmMetadata = sdmService.getObject(objectId, sdmCredentials, isSystemUser);
if (sdmMetadata == null || !sdmMetadata.has("succinctProperties")) {
return Collections.emptyList();
}
JSONObject succinctProperties = sdmMetadata.getJSONObject("succinctProperties");
Set<String> sdmResponseProperties = new HashSet<>(succinctProperties.keySet());

List<String> invalidProperties = new ArrayList<>();
for (String targetSdmProperty : customPropertiesInSDM) {
if (sdmResponseProperties.contains(targetSdmProperty)
&& !validSecondaryProperties.contains(targetSdmProperty)) {
invalidProperties.add(targetSdmProperty);
}
}
return invalidProperties;
} catch (IOException e) {
logger.error(
"Copy validation - Failed to fetch metadata for attachment {}: {}",
objectId,
e.getMessage());
return Collections.emptyList();
}
}

/** Builds and emits a warning message for copy failures */
private void buildAndWarnCopyFailures(
List<Map<String, String>> copyFailures, AttachmentCopyEventContext context) {
StringBuilder warningMessage =
new StringBuilder(SDMUtils.getErrorMessage("FAILED_TO_COPY_ATTACHMENTS_PREFIX"));
for (Map<String, String> failure : copyFailures) {
warningMessage
.append("- ObjectId: ")
.append(failure.get(OBJECT_ID_KEY))
.append(", Reason: ")
.append(failure.get(FAILURE_REASON_KEY))
.append("\n");
}
context.getMessages().warn(warningMessage.toString());
}

/**
* Moves attachments from source entity to target entity in SDM. Executes moves in parallel,
* updates database records, and cleans up source metadata. If any step fails, the operation is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,172 @@ void testCopyAttachments_AttachmentCopyFails_FolderExists_AttachmentsDeleted()
assertTrue(ex.getMessage().contains("Copy failed"));
}

@Test
void testCopyAttachments_InvalidSecondaryProperty_BlocksCopy() throws IOException {
// Mock SDMCredentials
SDMCredentials sdmCredentials = mock(SDMCredentials.class);
when(tokenHandler.getSDMCredentials()).thenReturn(sdmCredentials);

// Mock folder id retrieval
when(sdmService.getFolderIdByPath(any(), any(), any(), anyBoolean())).thenReturn(FOLDER_ID);

// Create mock context with custom property annotations on the entity
AttachmentCopyEventContext context = createMockContextWithCustomProperties();
when(context.getObjectIds()).thenReturn(List.of(OBJECT_ID));

// Mock secondary types and valid secondary properties from SDM
when(sdmService.getSecondaryTypes(any(), any(), anyBoolean()))
.thenReturn(List.of("secondaryType1"));
when(sdmService.getValidSecondaryProperties(any(), any(), any(), anyBoolean()))
.thenReturn(List.of("validProp1", "validProp2"));

// Mock getObject: SDM response contains "invalidCustomProp" which is NOT in valid list
JSONObject sdmMetadata = new JSONObject();
JSONObject succinctProperties = new JSONObject();
succinctProperties.put("cmis:name", "test.pdf");
succinctProperties.put("invalidCustomProp", "someValue");
sdmMetadata.put("succinctProperties", succinctProperties);
when(sdmService.getObject(eq(OBJECT_ID), any(), anyBoolean())).thenReturn(sdmMetadata);

// Act
sdmCustomServiceHandler.copyAttachments(context);

// Assert: copy should be blocked entirely, no copy operation performed
verify(sdmService, never())
.copyAttachment(any(), any(SDMCredentials.class), anyBoolean(), any());
// Warning message should be issued
verify(context.getMessages(), times(1)).warn(any(String.class));
verify(context, times(1)).setCompleted();
}

@Test
void testCopyAttachments_MixedValidAndInvalidSecondaryProps_CopiesValidRejectsInvalid()
throws IOException {
// Mock SDMCredentials
SDMCredentials sdmCredentials = mock(SDMCredentials.class);
when(tokenHandler.getSDMCredentials()).thenReturn(sdmCredentials);

// Mock folder id retrieval
when(sdmService.getFolderIdByPath(any(), any(), any(), anyBoolean())).thenReturn(FOLDER_ID);

String validObjectId = "validObjectId";
String invalidObjectId = "invalidObjectId";

// Create mock context with custom property annotations
AttachmentCopyEventContext context = createMockContextWithCustomProperties();
when(context.getObjectIds()).thenReturn(List.of(validObjectId, invalidObjectId));

// Mock secondary types and valid secondary properties
when(sdmService.getSecondaryTypes(any(), any(), anyBoolean()))
.thenReturn(List.of("secondaryType1"));
when(sdmService.getValidSecondaryProperties(any(), any(), any(), anyBoolean()))
.thenReturn(List.of("validProp1", "validProp2"));

// Mock getObject for valid attachment (does NOT have "invalidCustomProp" in response)
JSONObject validMetadata = new JSONObject();
JSONObject validProps = new JSONObject();
validProps.put("cmis:name", "valid.pdf");
validProps.put("validProp1", "someValue");
validMetadata.put("succinctProperties", validProps);
when(sdmService.getObject(eq(validObjectId), any(), anyBoolean())).thenReturn(validMetadata);

// Mock getObject for invalid attachment (HAS "invalidCustomProp" which is NOT in valid list)
JSONObject invalidMetadata = new JSONObject();
JSONObject invalidProps = new JSONObject();
invalidProps.put("cmis:name", "invalid.pdf");
invalidProps.put("invalidCustomProp", "someValue");
invalidMetadata.put("succinctProperties", invalidProps);
when(sdmService.getObject(eq(invalidObjectId), any(), anyBoolean()))
.thenReturn(invalidMetadata);

// Mock attachment metadata and copy for the valid attachment
CmisDocument cmisDocument = new CmisDocument();
cmisDocument.setType("sap-icon://document");
when(dbQuery.getAttachmentForObjectID(any(), any(), any(AttachmentCopyEventContext.class)))
.thenReturn(cmisDocument);

Map<String, String> attachmentData = new HashMap<>();
attachmentData.put("cmis:name", "valid.pdf");
attachmentData.put("cmis:contentStreamMimeType", "application/pdf");
attachmentData.put("cmis:objectId", "newCopiedObjectId");
when(sdmService.copyAttachment(any(), any(SDMCredentials.class), anyBoolean(), any()))
.thenReturn(attachmentData);

// Act
sdmCustomServiceHandler.copyAttachments(context);

// Assert: valid attachment should be copied, invalid one should be warned about
verify(sdmService, times(1))
.copyAttachment(any(), any(SDMCredentials.class), anyBoolean(), any());
verify(context.getMessages(), times(1)).warn(any(String.class));
verify(draftService, times(1)).newDraft(any());
verify(context, times(1)).setCompleted();
}

private AttachmentCopyEventContext createMockContextWithCustomProperties() {
AttachmentCopyEventContext context = mock(AttachmentCopyEventContext.class);
CdsElement mockAssociationElement = mock(CdsElement.class);
CdsAssociationType mockAssociationType = mock(CdsAssociationType.class);
CqnElementRef mockCqnElementRef = mock(CqnElementRef.class);

when(context.getParentEntity()).thenReturn("prefix.someIdentifier." + FACET);
when(context.getCompositionName()).thenReturn(FACET);
when(context.getUpId()).thenReturn(UP_ID);
when(context.getSystemUser()).thenReturn(true);
when(context.getObjectIds()).thenReturn(List.of(OBJECT_ID));

// Mock CdsModel and relevant entities and associations
CdsModel model = mock(CdsModel.class);
CdsEntity parentEntity = mock(CdsEntity.class);
CdsEntity draftEntity = mock(CdsEntity.class);
CdsEntity targetEntity = mock(CdsEntity.class);
CdsEntity compositionEntity = mock(CdsEntity.class);

// Mock composition element and its type
CdsElement compositionElement = mock(CdsElement.class);
CdsAssociationType compositionType = mock(CdsAssociationType.class);

// Setup expected behavior for model and parent entity
when(context.getModel()).thenReturn(model);
when(model.findEntity("prefix.someIdentifier." + FACET)).thenReturn(Optional.of(parentEntity));
when(model.findEntity("prefix.someIdentifier." + FACET + "." + FACET))
.thenReturn(Optional.of(compositionEntity));
when(model.findEntity(endsWith("_drafts"))).thenReturn(Optional.of(draftEntity));

// Mock the composition entity with a custom property annotation
CdsElement customPropertyElement = mock(CdsElement.class);
com.sap.cds.reflect.CdsAnnotation<Object> nameAnnotation =
mock(com.sap.cds.reflect.CdsAnnotation.class);
com.sap.cds.reflect.CdsType elementType = mock(com.sap.cds.reflect.CdsType.class);
when(elementType.isAssociation()).thenReturn(false);
when(customPropertyElement.getType()).thenReturn(elementType);
when(customPropertyElement.getName()).thenReturn("customField");
when(customPropertyElement.findAnnotation("SDM.Attachments.AdditionalProperty.name"))
.thenReturn(Optional.of(nameAnnotation));
when(nameAnnotation.getValue()).thenReturn("invalidCustomProp");
when(compositionEntity.elements()).thenReturn(Stream.of(customPropertyElement));

// Mock the composition element in parent entity
when(parentEntity.findElement(FACET)).thenReturn(Optional.of(compositionElement));
when(compositionElement.getType()).thenReturn(compositionType);
when(compositionType.isAssociation()).thenReturn(true);
when(compositionType.getTarget()).thenReturn(targetEntity);
when(targetEntity.getQualifiedName()).thenReturn("target.entity.name");

// Mock the draft entity's up_ association
when(draftEntity.findAssociation("up_")).thenReturn(Optional.of(mockAssociationElement));
when(mockAssociationElement.getType()).thenReturn(mockAssociationType);
when(mockAssociationType.refs()).thenReturn(Stream.of(mockCqnElementRef));
when(mockCqnElementRef.path()).thenReturn("ID");

// Mock messages
com.sap.cds.services.messages.Messages messages =
mock(com.sap.cds.services.messages.Messages.class);
when(context.getMessages()).thenReturn(messages);

return context;
}

private AttachmentCopyEventContext createMockContext() {
AttachmentCopyEventContext context = mock(AttachmentCopyEventContext.class);
CdsElement mockAssociationElement = mock(CdsElement.class);
Expand Down
Loading