Skip to content

Commit bbdf6f5

Browse files
authored
Merge branch 'develop' into hideUploadButton
2 parents 38d6d0f + e2678ac commit bbdf6f5

4 files changed

Lines changed: 299 additions & 2 deletions

File tree

sdm/src/main/java/com/sap/cds/sdm/constants/SDMErrorMessages.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,12 @@ private SDMErrorMessages() {
143143
public static final String INVALID_SECONDARY_PROPERTIES_FOR_MOVE_SUFFIX =
144144
". Attachment rolled back to source.";
145145
public static final String SDM_MOVE_OPERATION_FAILED = "SDM move operation failed";
146+
public static final String FAILED_TO_COPY_ATTACHMENTS_PREFIX =
147+
"Failed to copy the following attachments:\n";
148+
public static final String INVALID_SECONDARY_PROPERTIES_FOR_COPY_PREFIX =
149+
"Invalid secondary properties detected: ";
150+
public static final String INVALID_SECONDARY_PROPERTIES_FOR_COPY_SUFFIX =
151+
". Attachment not copied.";
146152
public static final String FAILED_TO_ACCESS_ERROR_KEY_FIELDS =
147153
"Failed to access SDM error key fields";
148154
public static final String FAILED_TO_ACCESS_ERROR_MESSAGES_FIELDS =

sdm/src/main/java/com/sap/cds/sdm/constants/SDMUIErrorKeys.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ private SDMUIErrorKeys() {}
8787
"SDM.invalidSecondaryPropertiesForMovePrefix";
8888
public static final String INVALID_SECONDARY_PROPERTIES_FOR_MOVE_SUFFIX_KEY =
8989
"SDM.invalidSecondaryPropertiesForMoveSuffix";
90+
public static final String FAILED_TO_COPY_ATTACHMENTS_PREFIX_KEY =
91+
"SDM.failedToCopyAttachmentsPrefix";
92+
public static final String INVALID_SECONDARY_PROPERTIES_FOR_COPY_PREFIX_KEY =
93+
"SDM.invalidSecondaryPropertiesForCopyPrefix";
94+
public static final String INVALID_SECONDARY_PROPERTIES_FOR_COPY_SUFFIX_KEY =
95+
"SDM.invalidSecondaryPropertiesForCopySuffix";
9096
public static final String MAX_COUNT_ERROR_MESSAGE_KEY = "SDM.maxCountErrorMessage";
9197

9298
public static Map<String, Object> getAllUIErrorKeys() {

sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMCustomServiceHandler.java

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,42 @@ public void copyAttachments(AttachmentCopyEventContext context) throws IOExcepti
127127
Boolean isSystemUser = context.getSystemUser();
128128

129129
SDMCredentials sdmCredentials = tokenHandler.getSDMCredentials();
130+
131+
List<String> objectIds = context.getObjectIds();
132+
133+
if (!customPropertiesInSDM.isEmpty()) {
134+
List<Map<String, String>> copyFailures =
135+
findAttachmentsWithInvalidSecondaryProperties(
136+
objectIds, customPropertiesInSDM, repositoryId, sdmCredentials, isSystemUser);
137+
if (!copyFailures.isEmpty()) {
138+
buildAndWarnCopyFailures(copyFailures, context);
139+
// Remove invalid objectIds and proceed with valid ones
140+
Set<String> invalidObjectIds =
141+
copyFailures.stream()
142+
.map(failure -> failure.get(OBJECT_ID_KEY))
143+
.collect(Collectors.toSet());
144+
objectIds =
145+
objectIds.stream()
146+
.filter(id -> !invalidObjectIds.contains(id))
147+
.collect(Collectors.toList());
148+
if (objectIds.isEmpty()) {
149+
context.setCompleted();
150+
logger.debug("END: Copy attachments event - all attachments have invalid properties");
151+
return;
152+
}
153+
logger.info(
154+
"Proceeding with {} valid attachments after filtering out {} invalid ones",
155+
objectIds.size(),
156+
invalidObjectIds.size());
157+
}
158+
}
159+
130160
// Check if folder exists before trying to create it
131161
boolean folderExists =
132162
sdmService.getFolderIdByPath(folderName, repositoryId, sdmCredentials, isSystemUser)
133163
!= null;
134164
String folderId = ensureFolderExists(folderName, repositoryId, sdmCredentials, isSystemUser);
135165

136-
List<String> objectIds = context.getObjectIds();
137-
138166
CopyAttachmentsRequest request =
139167
CopyAttachmentsRequest.builder()
140168
.context(context)
@@ -178,6 +206,97 @@ public void copyAttachments(AttachmentCopyEventContext context) throws IOExcepti
178206
logger.debug("END: Copy attachments event");
179207
}
180208

209+
/**
210+
* Checks source attachments for invalid secondary properties before copy. Returns a list of
211+
* failures if any attachment has invalid properties; returns empty list if all are valid
212+
*/
213+
private List<Map<String, String>> findAttachmentsWithInvalidSecondaryProperties(
214+
List<String> objectIds,
215+
Set<String> customPropertiesInSDM,
216+
String repositoryId,
217+
SDMCredentials sdmCredentials,
218+
Boolean isSystemUser)
219+
throws IOException {
220+
List<String> secondaryTypes =
221+
sdmService.getSecondaryTypes(repositoryId, sdmCredentials, isSystemUser);
222+
List<String> validSecondaryProperties =
223+
sdmService.getValidSecondaryProperties(
224+
secondaryTypes, sdmCredentials, repositoryId, isSystemUser);
225+
226+
List<Map<String, String>> failures = new ArrayList<>();
227+
for (String objectId : objectIds) {
228+
List<String> invalidProperties =
229+
getInvalidPropertiesForObject(
230+
objectId,
231+
customPropertiesInSDM,
232+
validSecondaryProperties,
233+
sdmCredentials,
234+
isSystemUser);
235+
if (!invalidProperties.isEmpty()) {
236+
Map<String, String> failure = new HashMap<>();
237+
failure.put(OBJECT_ID_KEY, objectId);
238+
failure.put(
239+
FAILURE_REASON_KEY,
240+
SDMUtils.getErrorMessage("INVALID_SECONDARY_PROPERTIES_FOR_COPY_PREFIX")
241+
+ String.join(", ", invalidProperties)
242+
+ SDMUtils.getErrorMessage("INVALID_SECONDARY_PROPERTIES_FOR_COPY_SUFFIX"));
243+
failures.add(failure);
244+
}
245+
}
246+
return failures;
247+
}
248+
249+
/**
250+
* Gets the list of invalid secondary properties for a single object from SDM. Returns empty list
251+
* if all properties are valid or if metadata cannot be fetched
252+
*/
253+
private List<String> getInvalidPropertiesForObject(
254+
String objectId,
255+
Set<String> customPropertiesInSDM,
256+
List<String> validSecondaryProperties,
257+
SDMCredentials sdmCredentials,
258+
Boolean isSystemUser) {
259+
try {
260+
JSONObject sdmMetadata = sdmService.getObject(objectId, sdmCredentials, isSystemUser);
261+
if (sdmMetadata == null || !sdmMetadata.has("succinctProperties")) {
262+
return Collections.emptyList();
263+
}
264+
JSONObject succinctProperties = sdmMetadata.getJSONObject("succinctProperties");
265+
Set<String> sdmResponseProperties = new HashSet<>(succinctProperties.keySet());
266+
267+
List<String> invalidProperties = new ArrayList<>();
268+
for (String targetSdmProperty : customPropertiesInSDM) {
269+
if (sdmResponseProperties.contains(targetSdmProperty)
270+
&& !validSecondaryProperties.contains(targetSdmProperty)) {
271+
invalidProperties.add(targetSdmProperty);
272+
}
273+
}
274+
return invalidProperties;
275+
} catch (IOException e) {
276+
logger.error(
277+
"Copy validation - Failed to fetch metadata for attachment {}: {}",
278+
objectId,
279+
e.getMessage());
280+
return Collections.emptyList();
281+
}
282+
}
283+
284+
/** Builds and emits a warning message for copy failures */
285+
private void buildAndWarnCopyFailures(
286+
List<Map<String, String>> copyFailures, AttachmentCopyEventContext context) {
287+
StringBuilder warningMessage =
288+
new StringBuilder(SDMUtils.getErrorMessage("FAILED_TO_COPY_ATTACHMENTS_PREFIX"));
289+
for (Map<String, String> failure : copyFailures) {
290+
warningMessage
291+
.append("- ObjectId: ")
292+
.append(failure.get(OBJECT_ID_KEY))
293+
.append(", Reason: ")
294+
.append(failure.get(FAILURE_REASON_KEY))
295+
.append("\n");
296+
}
297+
context.getMessages().warn(warningMessage.toString());
298+
}
299+
181300
/**
182301
* Moves attachments from source entity to target entity in SDM. Executes moves in parallel,
183302
* updates database records, and cleans up source metadata. If any step fails, the operation is

sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMCustomServiceHandlerTest.java

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,172 @@ void testCopyAttachments_AttachmentCopyFails_FolderExists_AttachmentsDeleted()
325325
assertTrue(ex.getMessage().contains("Copy failed"));
326326
}
327327

328+
@Test
329+
void testCopyAttachments_InvalidSecondaryProperty_BlocksCopy() throws IOException {
330+
// Mock SDMCredentials
331+
SDMCredentials sdmCredentials = mock(SDMCredentials.class);
332+
when(tokenHandler.getSDMCredentials()).thenReturn(sdmCredentials);
333+
334+
// Mock folder id retrieval
335+
when(sdmService.getFolderIdByPath(any(), any(), any(), anyBoolean())).thenReturn(FOLDER_ID);
336+
337+
// Create mock context with custom property annotations on the entity
338+
AttachmentCopyEventContext context = createMockContextWithCustomProperties();
339+
when(context.getObjectIds()).thenReturn(List.of(OBJECT_ID));
340+
341+
// Mock secondary types and valid secondary properties from SDM
342+
when(sdmService.getSecondaryTypes(any(), any(), anyBoolean()))
343+
.thenReturn(List.of("secondaryType1"));
344+
when(sdmService.getValidSecondaryProperties(any(), any(), any(), anyBoolean()))
345+
.thenReturn(List.of("validProp1", "validProp2"));
346+
347+
// Mock getObject: SDM response contains "invalidCustomProp" which is NOT in valid list
348+
JSONObject sdmMetadata = new JSONObject();
349+
JSONObject succinctProperties = new JSONObject();
350+
succinctProperties.put("cmis:name", "test.pdf");
351+
succinctProperties.put("invalidCustomProp", "someValue");
352+
sdmMetadata.put("succinctProperties", succinctProperties);
353+
when(sdmService.getObject(eq(OBJECT_ID), any(), anyBoolean())).thenReturn(sdmMetadata);
354+
355+
// Act
356+
sdmCustomServiceHandler.copyAttachments(context);
357+
358+
// Assert: copy should be blocked entirely, no copy operation performed
359+
verify(sdmService, never())
360+
.copyAttachment(any(), any(SDMCredentials.class), anyBoolean(), any());
361+
// Warning message should be issued
362+
verify(context.getMessages(), times(1)).warn(any(String.class));
363+
verify(context, times(1)).setCompleted();
364+
}
365+
366+
@Test
367+
void testCopyAttachments_MixedValidAndInvalidSecondaryProps_CopiesValidRejectsInvalid()
368+
throws IOException {
369+
// Mock SDMCredentials
370+
SDMCredentials sdmCredentials = mock(SDMCredentials.class);
371+
when(tokenHandler.getSDMCredentials()).thenReturn(sdmCredentials);
372+
373+
// Mock folder id retrieval
374+
when(sdmService.getFolderIdByPath(any(), any(), any(), anyBoolean())).thenReturn(FOLDER_ID);
375+
376+
String validObjectId = "validObjectId";
377+
String invalidObjectId = "invalidObjectId";
378+
379+
// Create mock context with custom property annotations
380+
AttachmentCopyEventContext context = createMockContextWithCustomProperties();
381+
when(context.getObjectIds()).thenReturn(List.of(validObjectId, invalidObjectId));
382+
383+
// Mock secondary types and valid secondary properties
384+
when(sdmService.getSecondaryTypes(any(), any(), anyBoolean()))
385+
.thenReturn(List.of("secondaryType1"));
386+
when(sdmService.getValidSecondaryProperties(any(), any(), any(), anyBoolean()))
387+
.thenReturn(List.of("validProp1", "validProp2"));
388+
389+
// Mock getObject for valid attachment (does NOT have "invalidCustomProp" in response)
390+
JSONObject validMetadata = new JSONObject();
391+
JSONObject validProps = new JSONObject();
392+
validProps.put("cmis:name", "valid.pdf");
393+
validProps.put("validProp1", "someValue");
394+
validMetadata.put("succinctProperties", validProps);
395+
when(sdmService.getObject(eq(validObjectId), any(), anyBoolean())).thenReturn(validMetadata);
396+
397+
// Mock getObject for invalid attachment (HAS "invalidCustomProp" which is NOT in valid list)
398+
JSONObject invalidMetadata = new JSONObject();
399+
JSONObject invalidProps = new JSONObject();
400+
invalidProps.put("cmis:name", "invalid.pdf");
401+
invalidProps.put("invalidCustomProp", "someValue");
402+
invalidMetadata.put("succinctProperties", invalidProps);
403+
when(sdmService.getObject(eq(invalidObjectId), any(), anyBoolean()))
404+
.thenReturn(invalidMetadata);
405+
406+
// Mock attachment metadata and copy for the valid attachment
407+
CmisDocument cmisDocument = new CmisDocument();
408+
cmisDocument.setType("sap-icon://document");
409+
when(dbQuery.getAttachmentForObjectID(any(), any(), any(AttachmentCopyEventContext.class)))
410+
.thenReturn(cmisDocument);
411+
412+
Map<String, String> attachmentData = new HashMap<>();
413+
attachmentData.put("cmis:name", "valid.pdf");
414+
attachmentData.put("cmis:contentStreamMimeType", "application/pdf");
415+
attachmentData.put("cmis:objectId", "newCopiedObjectId");
416+
when(sdmService.copyAttachment(any(), any(SDMCredentials.class), anyBoolean(), any()))
417+
.thenReturn(attachmentData);
418+
419+
// Act
420+
sdmCustomServiceHandler.copyAttachments(context);
421+
422+
// Assert: valid attachment should be copied, invalid one should be warned about
423+
verify(sdmService, times(1))
424+
.copyAttachment(any(), any(SDMCredentials.class), anyBoolean(), any());
425+
verify(context.getMessages(), times(1)).warn(any(String.class));
426+
verify(draftService, times(1)).newDraft(any());
427+
verify(context, times(1)).setCompleted();
428+
}
429+
430+
private AttachmentCopyEventContext createMockContextWithCustomProperties() {
431+
AttachmentCopyEventContext context = mock(AttachmentCopyEventContext.class);
432+
CdsElement mockAssociationElement = mock(CdsElement.class);
433+
CdsAssociationType mockAssociationType = mock(CdsAssociationType.class);
434+
CqnElementRef mockCqnElementRef = mock(CqnElementRef.class);
435+
436+
when(context.getParentEntity()).thenReturn("prefix.someIdentifier." + FACET);
437+
when(context.getCompositionName()).thenReturn(FACET);
438+
when(context.getUpId()).thenReturn(UP_ID);
439+
when(context.getSystemUser()).thenReturn(true);
440+
when(context.getObjectIds()).thenReturn(List.of(OBJECT_ID));
441+
442+
// Mock CdsModel and relevant entities and associations
443+
CdsModel model = mock(CdsModel.class);
444+
CdsEntity parentEntity = mock(CdsEntity.class);
445+
CdsEntity draftEntity = mock(CdsEntity.class);
446+
CdsEntity targetEntity = mock(CdsEntity.class);
447+
CdsEntity compositionEntity = mock(CdsEntity.class);
448+
449+
// Mock composition element and its type
450+
CdsElement compositionElement = mock(CdsElement.class);
451+
CdsAssociationType compositionType = mock(CdsAssociationType.class);
452+
453+
// Setup expected behavior for model and parent entity
454+
when(context.getModel()).thenReturn(model);
455+
when(model.findEntity("prefix.someIdentifier." + FACET)).thenReturn(Optional.of(parentEntity));
456+
when(model.findEntity("prefix.someIdentifier." + FACET + "." + FACET))
457+
.thenReturn(Optional.of(compositionEntity));
458+
when(model.findEntity(endsWith("_drafts"))).thenReturn(Optional.of(draftEntity));
459+
460+
// Mock the composition entity with a custom property annotation
461+
CdsElement customPropertyElement = mock(CdsElement.class);
462+
com.sap.cds.reflect.CdsAnnotation<Object> nameAnnotation =
463+
mock(com.sap.cds.reflect.CdsAnnotation.class);
464+
com.sap.cds.reflect.CdsType elementType = mock(com.sap.cds.reflect.CdsType.class);
465+
when(elementType.isAssociation()).thenReturn(false);
466+
when(customPropertyElement.getType()).thenReturn(elementType);
467+
when(customPropertyElement.getName()).thenReturn("customField");
468+
when(customPropertyElement.findAnnotation("SDM.Attachments.AdditionalProperty.name"))
469+
.thenReturn(Optional.of(nameAnnotation));
470+
when(nameAnnotation.getValue()).thenReturn("invalidCustomProp");
471+
when(compositionEntity.elements()).thenReturn(Stream.of(customPropertyElement));
472+
473+
// Mock the composition element in parent entity
474+
when(parentEntity.findElement(FACET)).thenReturn(Optional.of(compositionElement));
475+
when(compositionElement.getType()).thenReturn(compositionType);
476+
when(compositionType.isAssociation()).thenReturn(true);
477+
when(compositionType.getTarget()).thenReturn(targetEntity);
478+
when(targetEntity.getQualifiedName()).thenReturn("target.entity.name");
479+
480+
// Mock the draft entity's up_ association
481+
when(draftEntity.findAssociation("up_")).thenReturn(Optional.of(mockAssociationElement));
482+
when(mockAssociationElement.getType()).thenReturn(mockAssociationType);
483+
when(mockAssociationType.refs()).thenReturn(Stream.of(mockCqnElementRef));
484+
when(mockCqnElementRef.path()).thenReturn("ID");
485+
486+
// Mock messages
487+
com.sap.cds.services.messages.Messages messages =
488+
mock(com.sap.cds.services.messages.Messages.class);
489+
when(context.getMessages()).thenReturn(messages);
490+
491+
return context;
492+
}
493+
328494
private AttachmentCopyEventContext createMockContext() {
329495
AttachmentCopyEventContext context = mock(AttachmentCopyEventContext.class);
330496
CdsElement mockAssociationElement = mock(CdsElement.class);

0 commit comments

Comments
 (0)