Skip to content

Commit c63ef2f

Browse files
committed
Update error messages and implement maxCount validation for move
1 parent f7fa125 commit c63ef2f

3 files changed

Lines changed: 220 additions & 14 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ private SDMConstants() {
107107
public static final String INVALID_SECONDARY_PROPERTIES_PREFIX =
108108
"Invalid secondary properties detected: ";
109109
public static final String INVALID_SECONDARY_PROPERTIES_SUFFIX =
110-
". These properties are not supported in the target entity. Attachment rolled back to source.";
110+
". Attachment rolled back to source.";
111111
public static final String FAILED_TO_MOVE_ATTACHMENT = "Failed to move attachment";
112112
public static final String FAILED_TO_MOVE_ATTACHMENT_MSG = "SDM.Move.failedToMoveAttachmentError";
113113
public static final String MOVE_OPERATION_PARTIAL_FAILURE =

sdm/src/main/java/com/sap/cds/sdm/model/AttachmentMoveContext.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public class AttachmentMoveContext {
1414
private final CdsEntity targetEntity;
1515
private final AttachmentProcessingResults processingResults;
1616
private final List<Map<String, String>> failedAttachments;
17+
private List<String> invalidProperties; // Mutable field to track validation failures
1718

1819
public AttachmentMoveContext(
1920
String objectId,
@@ -60,6 +61,14 @@ public List<Map<String, String>> getFailedAttachments() {
6061
return Collections.unmodifiableList(failedAttachments);
6162
}
6263

64+
public List<String> getInvalidProperties() {
65+
return invalidProperties;
66+
}
67+
68+
public void setInvalidProperties(List<String> invalidProperties) {
69+
this.invalidProperties = invalidProperties;
70+
}
71+
6372
/**
6473
* Internal method for adding failed attachments during processing. For internal use only - do not
6574
* expose to external callers.

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

Lines changed: 210 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.sap.cds.sdm.service.handler;
22

3+
import com.sap.cds.Result;
34
import com.sap.cds.ql.Insert;
45
import com.sap.cds.reflect.CdsAssociationType;
56
import com.sap.cds.reflect.CdsElement;
@@ -24,6 +25,7 @@
2425
import com.sap.cds.sdm.persistence.DBQuery;
2526
import com.sap.cds.sdm.service.RegisterService;
2627
import com.sap.cds.sdm.service.SDMService;
28+
import com.sap.cds.sdm.utilities.SDMUtils;
2729
import com.sap.cds.services.EventContext;
2830
import com.sap.cds.services.ServiceException;
2931
import com.sap.cds.services.draft.DraftService;
@@ -183,6 +185,16 @@ public void moveAttachments(AttachmentMoveEventContext context) throws IOExcepti
183185

184186
SDMCredentials sdmCredentials = tokenHandler.getSDMCredentials();
185187

188+
// Check maxCount constraint before attempting move
189+
List<Map<String, String>> failedAttachments =
190+
checkMaxCountConstraintForMove(context, parentEntity, compositionName, upID, objectIds);
191+
if (!failedAttachments.isEmpty()) {
192+
// All attachments failed maxCount validation
193+
context.setFailedAttachments(failedAttachments);
194+
context.setCompleted();
195+
return;
196+
}
197+
186198
// Ensure target folder exists in SDM before attempting moves
187199
TargetFolderInfo folderInfo =
188200
ensureTargetFolderReady(
@@ -206,17 +218,17 @@ public void moveAttachments(AttachmentMoveEventContext context) throws IOExcepti
206218

207219
List<List<String>> movedAttachmentsMetadata = moveResult.getMovedAttachmentsMetadata();
208220
List<CmisDocument> populatedDocuments = moveResult.getPopulatedDocuments();
209-
List<Map<String, String>> failedAttachments = moveResult.getFailedAttachments();
221+
List<Map<String, String>> moveFailures = moveResult.getFailedAttachments();
210222
List<String> successfulObjectIds = moveResult.getSuccessfulObjectIds();
211223

212224
// Return failed attachments to caller
213-
context.setFailedAttachments(new ArrayList<>(failedAttachments));
225+
context.setFailedAttachments(new ArrayList<>(moveFailures));
214226

215227
// Show warning if there are failures
216-
if (!failedAttachments.isEmpty()) {
228+
if (!moveFailures.isEmpty()) {
217229
StringBuilder warningMessage =
218230
new StringBuilder("Failed to move the following attachments:\n");
219-
for (Map<String, String> failure : failedAttachments) {
231+
for (Map<String, String> failure : moveFailures) {
220232
warningMessage
221233
.append(" - ObjectId: ")
222234
.append(failure.get(OBJECT_ID_KEY))
@@ -261,6 +273,126 @@ public void moveAttachments(AttachmentMoveEventContext context) throws IOExcepti
261273
context.setCompleted();
262274
}
263275

276+
/**
277+
* Checks if moving attachments would exceed the maxCount constraint on the target entity.
278+
*
279+
* @param context the move event context
280+
* @param parentEntity the parent entity name
281+
* @param compositionName the composition name
282+
* @param upID the up ID
283+
* @param objectIds list of attachment object IDs to move
284+
* @return list of failed attachments if constraint is violated, empty list otherwise
285+
*/
286+
private List<Map<String, String>> checkMaxCountConstraintForMove(
287+
AttachmentMoveEventContext context,
288+
String parentEntity,
289+
String compositionName,
290+
String upID,
291+
List<String> objectIds) {
292+
List<Map<String, String>> failedAttachments = new ArrayList<>();
293+
294+
try {
295+
// Get target attachment entity
296+
Optional<CdsEntity> targetEntityOptional =
297+
context.getModel().findEntity(parentEntity + "." + compositionName);
298+
if (targetEntityOptional.isEmpty()) {
299+
logger.warn(
300+
"Target entity {}.{} not found, skipping maxCount validation",
301+
parentEntity,
302+
compositionName);
303+
return failedAttachments;
304+
}
305+
306+
CdsEntity targetAttachmentEntity = targetEntityOptional.get();
307+
308+
// Get maxCount and error message from annotations
309+
String errorMessageCount =
310+
SDMUtils.getAttachmentCountAndMessage(
311+
context.getModel().entities().toList(), targetAttachmentEntity);
312+
String[] maxCountArr = errorMessageCount.split("__");
313+
long maxCount = Long.parseLong(maxCountArr[0]);
314+
315+
// If maxCount is 0 or negative, no limit is enforced
316+
if (maxCount <= 0) {
317+
return failedAttachments;
318+
}
319+
320+
// Get target attachment draft entity for querying existing attachments
321+
Optional<CdsEntity> draftEntityOptional =
322+
context.getModel().findEntity(targetAttachmentEntity.getQualifiedName() + "_drafts");
323+
if (draftEntityOptional.isEmpty()) {
324+
logger.warn(
325+
"Draft entity for {} not found, skipping maxCount validation",
326+
targetAttachmentEntity.getQualifiedName());
327+
return failedAttachments;
328+
}
329+
330+
CdsEntity attachmentDraftEntity = draftEntityOptional.get();
331+
String upIdKey = SDMUtils.getUpIdKey(attachmentDraftEntity);
332+
333+
// Count existing attachments in target entity
334+
Result result =
335+
dbQuery.getAttachmentsForUPIDAndRepository(
336+
attachmentDraftEntity, persistenceService, upID, upIdKey);
337+
long existingCount = result.rowCount();
338+
long totalCountAfterMove = existingCount + objectIds.size();
339+
340+
logger.info(
341+
"MaxCount validation - Target entity: {}, MaxCount: {}, Existing: {}, Moving: {},"
342+
+ " Total after move: {}",
343+
targetAttachmentEntity.getQualifiedName(),
344+
maxCount,
345+
existingCount,
346+
objectIds.size(),
347+
totalCountAfterMove);
348+
349+
// Check if total would exceed maxCount
350+
if (totalCountAfterMove > maxCount) {
351+
String errorMessage = maxCountArr[1];
352+
String failureReason;
353+
354+
if (errorMessage != null && !"null".equalsIgnoreCase(errorMessage)) {
355+
// Use custom error message from annotation
356+
failureReason = errorMessage;
357+
} else {
358+
// Use default error message
359+
failureReason =
360+
String.format(
361+
"Cannot move %d attachment(s). Target entity allows maximum %d attachments, and"
362+
+ " already has %d. Maximum count would be exceeded.",
363+
objectIds.size(), maxCount, existingCount);
364+
}
365+
366+
logger.warn(
367+
"Move operation rejected: Total count {} exceeds maxCount {}. Marking all {} attachments"
368+
+ " as failed.",
369+
totalCountAfterMove,
370+
maxCount,
371+
objectIds.size());
372+
373+
// Mark all attachments as failed
374+
for (String objectId : objectIds) {
375+
Map<String, String> failure = new HashMap<>();
376+
failure.put(OBJECT_ID_KEY, objectId);
377+
failure.put(FAILURE_REASON_KEY, failureReason);
378+
failedAttachments.add(failure);
379+
}
380+
381+
// Show warning message
382+
context.getMessages().warn(failureReason);
383+
}
384+
} catch (Exception e) {
385+
logger.error(
386+
"Error during maxCount validation for move operation: {}. Proceeding without"
387+
+ " validation.",
388+
e.getMessage(),
389+
e);
390+
// Don't block the move operation if validation fails
391+
}
392+
393+
return failedAttachments;
394+
}
395+
264396
/**
265397
* Updates database with moved attachments and cleans up source entity metadata.
266398
*
@@ -631,6 +763,73 @@ private void handleValidationFailure(
631763
failedAttachments.add(failure);
632764
}
633765

766+
/**
767+
* Parses SDM error message to extract meaningful failure reason.
768+
*
769+
* @param errorMessage The raw error message from SDM
770+
* @return A user-friendly failure reason
771+
*/
772+
private String parseSDMErrorMessage(String errorMessage) {
773+
if (errorMessage == null || errorMessage.isEmpty()) {
774+
return SDMConstants.SDM_MOVE_OPERATION_FAILED;
775+
}
776+
777+
// Check for nameConstraintViolation (duplicate file)
778+
if (errorMessage.contains("nameConstraintViolation")) {
779+
// Extract the meaningful part: "Child 6.pdf with Id ... already exists"
780+
int colonIndex = errorMessage.indexOf(":");
781+
if (colonIndex != -1 && colonIndex + 1 < errorMessage.length()) {
782+
return errorMessage.substring(colonIndex + 1).trim();
783+
}
784+
}
785+
786+
// Check for other common SDM errors and extract meaningful parts
787+
// For now, return the full message as fallback
788+
return errorMessage;
789+
}
790+
791+
/**
792+
* Builds a detailed validation failure message including invalid properties if available.
793+
*
794+
* @param moveContext The move context containing validation state
795+
* @param exception The exception that occurred
796+
* @return A detailed failure message
797+
*/
798+
private String buildValidationFailureMessage(
799+
AttachmentMoveContext moveContext, Exception exception) {
800+
// Check if we have invalid properties information in the context
801+
if (moveContext.getInvalidProperties() != null
802+
&& !moveContext.getInvalidProperties().isEmpty()) {
803+
return SDMConstants.INVALID_SECONDARY_PROPERTIES_PREFIX
804+
+ String.join(", ", moveContext.getInvalidProperties())
805+
+ SDMConstants.INVALID_SECONDARY_PROPERTIES_SUFFIX;
806+
}
807+
808+
// Detect specific failure types and provide meaningful messages
809+
String exceptionType = exception.getClass().getSimpleName();
810+
String exceptionMessage = exception.getMessage();
811+
812+
// Database fetch failure
813+
if (exceptionType.contains("ServiceException")
814+
|| exceptionMessage != null && exceptionMessage.contains("database")) {
815+
return "Failed to retrieve attachment metadata from database. Attachment rolled back to source.";
816+
}
817+
818+
// JSON parsing failure
819+
if (exceptionType.contains("JSONException")
820+
|| exceptionMessage != null && exceptionMessage.contains("JSON")) {
821+
return "Failed to parse SDM response. Attachment rolled back to source.";
822+
}
823+
824+
// Processing/other failures with specific message
825+
if (exceptionMessage != null && !exceptionMessage.isEmpty()) {
826+
return "Processing failed: " + exceptionMessage + ". Attachment rolled back to source.";
827+
}
828+
829+
// Generic fallback
830+
return "Attachment processing failed. Attachment rolled back to source.";
831+
}
832+
634833
/**
635834
* Processes a single attachment move: Move in SDM → Validate → Process or Rollback.
636835
*
@@ -723,6 +922,8 @@ private void processSingleAttachmentMove(AttachmentMoveContext moveContext) {
723922

724923
if (!invalidProperties.isEmpty()) {
725924
// Step 3a: Validation failed → Rollback immediately
925+
// Store invalid properties in context for detailed error message
926+
moveContext.setInvalidProperties(invalidProperties);
726927
handleValidationFailure(
727928
moveContext.getObjectId(),
728929
invalidProperties,
@@ -757,10 +958,9 @@ private void processSingleAttachmentMove(AttachmentMoveContext moveContext) {
757958
e.getMessage());
758959
Map<String, String> failure = new HashMap<>();
759960
failure.put(OBJECT_ID_KEY, moveContext.getObjectId());
760-
// Pass through the actual SDM error message for clarity
761-
failure.put(
762-
FAILURE_REASON_KEY,
763-
e.getMessage() != null ? e.getMessage() : SDMConstants.SDM_MOVE_OPERATION_FAILED);
961+
// Parse SDM error message to extract meaningful failure reason
962+
String failureReason = parseSDMErrorMessage(e.getMessage());
963+
failure.put(FAILURE_REASON_KEY, failureReason);
764964
moveContext.addFailedAttachment(failure);
765965
} catch (Exception e) {
766966
// Validation/processing failed
@@ -771,11 +971,8 @@ private void processSingleAttachmentMove(AttachmentMoveContext moveContext) {
771971
e);
772972
Map<String, String> failure = new HashMap<>();
773973
failure.put(OBJECT_ID_KEY, moveContext.getObjectId());
774-
// Provide detailed validation error with context
775-
String detailedReason =
776-
e.getMessage() != null && !e.getMessage().isEmpty()
777-
? SDMConstants.VALIDATION_FAILED_PREFIX + e.getMessage()
778-
: SDMConstants.VALIDATION_FAILED_DEFAULT_MESSAGE;
974+
// Provide detailed validation error with invalid properties if available
975+
String detailedReason = buildValidationFailureMessage(moveContext, e);
779976
failure.put(FAILURE_REASON_KEY, detailedReason);
780977
moveContext.addFailedAttachment(failure);
781978
}

0 commit comments

Comments
 (0)