Skip to content

Commit b3db991

Browse files
committed
Support for disabling Upload button when max count is reached
1 parent 588490f commit b3db991

5 files changed

Lines changed: 1255 additions & 8 deletions

File tree

sdm/src/main/java/com/sap/cds/sdm/persistence/DBQuery.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,4 +1136,76 @@ private String resolveUpIdKey(CdsModel model, String parentEntity, String compos
11361136

11371137
return null;
11381138
}
1139+
1140+
/**
1141+
* Looks up the parent ID (up__ID) of an attachment by its objectId.
1142+
*
1143+
* @param attachmentEntity the attachment entity to query
1144+
* @param persistenceService the persistence service
1145+
* @param objectId the SDM objectId of the attachment
1146+
* @param upIdKey the field name for the parent ID (e.g. "up__ID")
1147+
* @return the parent ID string, or null if not found
1148+
*/
1149+
public String getUpIdByObjectId(
1150+
CdsEntity attachmentEntity,
1151+
PersistenceService persistenceService,
1152+
String objectId,
1153+
String upIdKey) {
1154+
logger.debug(
1155+
"Fetching {} for objectId: {} from entity: {}",
1156+
upIdKey,
1157+
objectId,
1158+
attachmentEntity.getQualifiedName());
1159+
CqnSelect q =
1160+
Select.from(attachmentEntity)
1161+
.columns(upIdKey)
1162+
.where(doc -> doc.get("objectId").eq(objectId));
1163+
Result result = persistenceService.run(q);
1164+
return result
1165+
.first()
1166+
.map(row -> row.get(upIdKey) != null ? row.get(upIdKey).toString() : null)
1167+
.orElse(null);
1168+
}
1169+
1170+
/**
1171+
* Updates the isUploadable flag on the parent entity row that owns the attachment composition.
1172+
* One row updated instead of N attachment rows.
1173+
*
1174+
* @param parentEntity the parent CDS entity (e.g. Books or Books_drafts)
1175+
* @param persistenceService the persistence service
1176+
* @param parentId the parent entity's key value
1177+
* @param parentKeyField the parent entity's key field name (e.g. "ID")
1178+
* @param isUploadable the new value of the isUploadable flag
1179+
*/
1180+
public long updateIsUploadableOnParentEntity(
1181+
CdsEntity parentEntity,
1182+
PersistenceService persistenceService,
1183+
String parentId,
1184+
String parentKeyField,
1185+
String facetField,
1186+
boolean isUploadable) {
1187+
logger.debug(
1188+
"Updating {}={} on parent entity: {} for {}={}",
1189+
facetField,
1190+
isUploadable,
1191+
parentEntity.getQualifiedName(),
1192+
parentKeyField,
1193+
parentId);
1194+
Map<String, Object> updatedFields = new HashMap<>();
1195+
updatedFields.put(facetField, isUploadable);
1196+
CqnUpdate updateQuery =
1197+
Update.entity(parentEntity)
1198+
.data(updatedFields)
1199+
.where(e -> e.get(parentKeyField).eq(parentId));
1200+
Result updateResult = persistenceService.run(updateQuery);
1201+
logger.info(
1202+
"Updated {}={} for {} row(s) on {} where {}={}",
1203+
facetField,
1204+
isUploadable,
1205+
updateResult.rowCount(),
1206+
parentEntity.getQualifiedName(),
1207+
parentKeyField,
1208+
parentId);
1209+
return updateResult.rowCount();
1210+
}
11391211
}

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

Lines changed: 207 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ public void markAttachmentAsDeleted(AttachmentMarkAsDeletedEventContext context)
9393
String objectId = contextValues[0];
9494
String folderId = contextValues[1];
9595
String entity = contextValues[2];
96+
// 4th segment is upID embedded at creation time (absent in old contentId format → null)
97+
String upIDFromContentId = contextValues.length >= 4 ? contextValues[3] : null;
9698
logger.debug(
9799
"Processing deletion - objectId: {}, folderId: {}, entity: {}",
98100
objectId,
@@ -118,6 +120,8 @@ public void markAttachmentAsDeleted(AttachmentMarkAsDeletedEventContext context)
118120
logger.debug("ObjectId {} is still referenced, not deleting", objectId);
119121
}
120122
}
123+
// After SDM deletion, check if count drops below maxCount and update isUploadable.
124+
updateIsUploadableOnDelete(context, objectId, entity, upIDFromContentId);
121125
} else {
122126
logger.warn("Invalid contentId format for deletion: {}", contentId);
123127
}
@@ -513,8 +517,16 @@ private void handleCreateDocumentResult(
513517
cmisDocument.setObjectId(existing.getObjectId());
514518
cmisDocument.setFolderId(existing.getFolderId());
515519
cmisDocument.setMimeType(existing.getMimeType());
520+
String parentIdForContentId =
521+
cmisDocument.getParentId() != null ? cmisDocument.getParentId() : "";
516522
eventContext.setContentId(
517-
existing.getObjectId() + ":" + existing.getFolderId() + ":" + activeEntityName);
523+
existing.getObjectId()
524+
+ ":"
525+
+ existing.getFolderId()
526+
+ ":"
527+
+ activeEntityName
528+
+ ":"
529+
+ parentIdForContentId);
518530
eventContext.getData().setStatus("Clean");
519531
eventContext.getData().setContent(null);
520532
eventContext.setCompleted();
@@ -579,6 +591,10 @@ private void handleCreateDocumentResult(
579591
logger.debug("Updating draft entity attachment record");
580592
dbQuery.addAttachmentToDraft(attachmentEntity, persistenceService, cmisDocument);
581593
finalizeContext(eventContext, cmisDocument);
594+
// After a successful upload, check whether the maxCount has been reached and
595+
// update isUploadable accordingly so the UI can hide the Upload button.
596+
checkAndUpdateIsUploadableOnCreate(
597+
eventContext, attachmentEntity, cmisDocument.getParentId());
582598
} else {
583599
// Active entity - call finalizeContext() just like draft path.
584600
// This sets contentId and calls setCompleted(), which:
@@ -600,6 +616,8 @@ private void handleCreateDocumentResult(
600616
metadata.put("uploadStatus", cmisDocument.getUploadStatus());
601617
metadata.put("mimeType", cmisDocument.getMimeType());
602618
metadata.put("attachmentEntity", attachmentEntity);
619+
// Store the parent ID so the @After ApplicationService handler can update isUploadable.
620+
metadata.put("upID", cmisDocument.getParentId());
603621
SDM_METADATA_THREADLOCAL.set(metadata);
604622

605623
// finalizeContext sets contentId and calls setCompleted()
@@ -610,15 +628,202 @@ private void handleCreateDocumentResult(
610628
}
611629
}
612630

631+
/**
632+
* After a successful upload, checks whether the attachment count for the parent entity has
633+
* reached the configured maxCount. If so, sets the facet-specific uploadable flag (e.g.
634+
* isAttachmentsUploadable, isReferencesUploadable) to false on the parent entity row so the UI
635+
* hides the Upload button for that facet.
636+
*/
637+
private void checkAndUpdateIsUploadableOnCreate(
638+
AttachmentCreateEventContext eventContext, CdsEntity attachmentEntity, String upID) {
639+
if (upID == null || upID.isEmpty()) return;
640+
try {
641+
String upIdKey = SDMUtils.getUpIdKey(attachmentEntity);
642+
if (upIdKey.isEmpty()) return;
643+
Result countResult =
644+
dbQuery.getAttachmentsForUPIDAndRepository(
645+
attachmentEntity, persistenceService, upID, upIdKey);
646+
long count = countResult.rowCount();
647+
Long maxCount =
648+
SDMUtils.getAttachmentCountAndMessage(
649+
eventContext.getModel().entities().toList(), eventContext.getAttachmentEntity());
650+
if (maxCount > 0 && count >= maxCount) {
651+
String facetField = deriveFacetFieldName(attachmentEntity.getQualifiedName());
652+
logger.info(
653+
"Max attachment count ({}) reached for upID: {}. Setting {}=false.",
654+
maxCount,
655+
upID,
656+
facetField);
657+
String parentKeyField = upIdKey.replaceFirst("^up__", "");
658+
updateParentIsUploadable(
659+
eventContext.getModel(),
660+
attachmentEntity.getQualifiedName(),
661+
upID,
662+
parentKeyField,
663+
facetField,
664+
false);
665+
}
666+
} catch (Exception e) {
667+
logger.warn("Error updating isUploadable on create: {}", e.getMessage());
668+
}
669+
}
670+
671+
/**
672+
* After an SDM document deletion, checks whether the remaining attachment count for the parent
673+
* entity has dropped below maxCount. If so, sets isUploadable=true on all remaining attachment
674+
* rows for that parent so the UI shows the Upload button again.
675+
*
676+
* <p>The record being deleted is still present in the DB at the time this method runs; count-1
677+
* represents the count after the pending DB deletion.
678+
*/
679+
private void updateIsUploadableOnDelete(
680+
AttachmentMarkAsDeletedEventContext context,
681+
String objectId,
682+
String entity,
683+
String upIDFromContentId) {
684+
try {
685+
CdsEntity attachmentEntity = null;
686+
String upIdKey = "";
687+
// Use upID embedded in contentId when available (avoids DB lookup which fails when
688+
// markAttachmentAsDeleted fires after the attachment row is already removed during
689+
// draftActivate).
690+
String upID =
691+
(upIDFromContentId != null && !upIDFromContentId.isEmpty()) ? upIDFromContentId : null;
692+
693+
// Resolve the attachment entity (needed for upIdKey, facetField, and count query).
694+
// Try the draft entity first — the count query runs against whichever table is current.
695+
Optional<CdsEntity> draftEntityOpt = context.getModel().findEntity(entity + "_drafts");
696+
if (draftEntityOpt.isPresent()) {
697+
attachmentEntity = draftEntityOpt.get();
698+
upIdKey = SDMUtils.getUpIdKey(attachmentEntity);
699+
if (upID == null && !upIdKey.isEmpty()) {
700+
// Backward compat: DB lookup for attachments created before the upID-in-contentId fix
701+
upID = dbQuery.getUpIdByObjectId(attachmentEntity, persistenceService, objectId, upIdKey);
702+
}
703+
}
704+
705+
// Fall back to the active entity (direct deletes without draft, or old contentId format)
706+
if (upID == null) {
707+
Optional<CdsEntity> activeEntityOpt = context.getModel().findEntity(entity);
708+
if (activeEntityOpt.isPresent()) {
709+
attachmentEntity = activeEntityOpt.get();
710+
upIdKey = SDMUtils.getUpIdKey(attachmentEntity);
711+
if (!upIdKey.isEmpty()) {
712+
upID =
713+
dbQuery.getUpIdByObjectId(attachmentEntity, persistenceService, objectId, upIdKey);
714+
}
715+
}
716+
}
717+
718+
if (upID == null || attachmentEntity == null || upIdKey.isEmpty()) {
719+
logger.warn("Could not determine upID for objectId: {} in entity: {}", objectId, entity);
720+
return;
721+
}
722+
723+
// Count still includes the record being deleted (DB deletion happens after this handler).
724+
Result countResult =
725+
dbQuery.getAttachmentsForUPIDAndRepository(
726+
attachmentEntity, persistenceService, upID, upIdKey);
727+
long count = countResult.rowCount();
728+
729+
// Determine maxCount using the base (non-draft) attachment entity.
730+
CdsEntity baseEntity = context.getModel().findEntity(entity).orElse(attachmentEntity);
731+
Long maxCount =
732+
SDMUtils.getAttachmentCountAndMessage(context.getModel().entities().toList(), baseEntity);
733+
734+
if (maxCount > 0 && (count - 1) < maxCount) {
735+
String facetField = deriveFacetFieldName(attachmentEntity.getQualifiedName());
736+
logger.info(
737+
"Attachment count will drop to {} (below maxCount={}). Setting {}=true for upID: {}",
738+
count - 1,
739+
maxCount,
740+
facetField,
741+
upID);
742+
String parentKeyField = upIdKey.replaceFirst("^up__", "");
743+
updateParentIsUploadable(
744+
context.getModel(),
745+
attachmentEntity.getQualifiedName(),
746+
upID,
747+
parentKeyField,
748+
facetField,
749+
true);
750+
}
751+
} catch (Exception e) {
752+
logger.warn("Error updating isUploadable on delete: {}", e.getMessage());
753+
}
754+
}
755+
756+
/**
757+
* Updates the facet-specific uploadable flag on the parent entity row (e.g. Books or
758+
* Books_drafts) that owns the attachment composition. Draft context is detected from the
759+
* attachment entity name suffix.
760+
*
761+
* @param facetField the parent entity field to update (e.g. "isAttachmentsUploadable")
762+
*/
763+
private void updateParentIsUploadable(
764+
CdsModel model,
765+
String attachmentEntityName,
766+
String upID,
767+
String parentKeyField,
768+
String facetField,
769+
boolean value) {
770+
String parentBaseName = deriveParentEntityName(attachmentEntityName);
771+
boolean isDraft = attachmentEntityName.endsWith("_drafts");
772+
if (isDraft) {
773+
Optional<CdsEntity> parentDraftOpt = model.findEntity(parentBaseName + "_drafts");
774+
if (parentDraftOpt.isPresent()) {
775+
long updated =
776+
dbQuery.updateIsUploadableOnParentEntity(
777+
parentDraftOpt.get(), persistenceService, upID, parentKeyField, facetField, value);
778+
if (updated > 0) {
779+
// Draft row updated — no need to touch active entity now (draftActivate will copy it)
780+
return;
781+
}
782+
// 0 rows updated: draft was already activated and the draft row is gone.
783+
// Fall through to update the active entity directly.
784+
logger.debug("Draft parent had no row for upID {}, falling through to active entity", upID);
785+
}
786+
}
787+
Optional<CdsEntity> parentActiveOpt = model.findEntity(parentBaseName);
788+
if (parentActiveOpt.isPresent()) {
789+
dbQuery.updateIsUploadableOnParentEntity(
790+
parentActiveOpt.get(), persistenceService, upID, parentKeyField, facetField, value);
791+
} else {
792+
logger.warn("Parent entity not found for attachment entity: {}", attachmentEntityName);
793+
}
794+
}
795+
796+
/**
797+
* Derives the parent entity field name for the uploadable flag from the attachment entity's
798+
* qualified name. The facet name (last segment, ignoring _drafts suffix) is capitalized and
799+
* wrapped: e.g. "...Books.attachments" → "isAttachmentsUploadable", "...Books.references" →
800+
* "isReferencesUploadable".
801+
*/
802+
private String deriveFacetFieldName(String attachmentEntityQualifiedName) {
803+
String base = attachmentEntityQualifiedName.replace("_drafts", "");
804+
int lastDot = base.lastIndexOf('.');
805+
String facet = lastDot >= 0 ? base.substring(lastDot + 1) : base;
806+
return "is" + Character.toUpperCase(facet.charAt(0)) + facet.substring(1) + "Uploadable";
807+
}
808+
809+
private String deriveParentEntityName(String qualifiedName) {
810+
String base = qualifiedName.replace("_drafts", "");
811+
int lastDot = base.lastIndexOf('.');
812+
return lastDot > 0 ? base.substring(0, lastDot) : base;
813+
}
814+
613815
private void finalizeContext(
614816
AttachmentCreateEventContext eventContext, CmisDocument cmisDocument) {
615817
logger.debug("Finalizing attachment context for objectId: {}", cmisDocument.getObjectId());
818+
String upIdSegment = cmisDocument.getParentId() != null ? cmisDocument.getParentId() : "";
616819
eventContext.setContentId(
617820
cmisDocument.getObjectId()
618821
+ ":"
619822
+ cmisDocument.getFolderId()
620823
+ ":"
621-
+ eventContext.getAttachmentEntity().getQualifiedName());
824+
+ eventContext.getAttachmentEntity().getQualifiedName()
825+
+ ":"
826+
+ upIdSegment);
622827
eventContext.getData().setStatus("Clean");
623828
eventContext.getData().setContent(null);
624829
eventContext.setCompleted();

sdm/src/main/resources/cds/com.sap.cds/sdm/attachments.cds

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ extend aspect Attachments with {
2222
type : String @(UI: {IsImageURL: true}) default 'sap-icon://document';
2323
uploadStatus : UploadStatusCode default 'uploading' @readonly ;
2424
uploadStatusNav : Association to one UploadScanStates on uploadStatusNav.code = uploadStatus;
25-
}
26-
entity UploadScanStates : CodeList {
27-
key code : UploadStatusCode @Common.Text: name @Common.TextArrangement: #TextOnly;
28-
name : String(64) ;
29-
criticality : Integer @UI.Hidden;
30-
}
25+
}
26+
27+
entity UploadScanStates : CodeList {
28+
key code : UploadStatusCode @Common.Text: name @Common.TextArrangement: #TextOnly;
29+
name : String(64) ;
30+
criticality : Integer @UI.Hidden;
31+
}
3132

3233
annotate Attachments with @UI: {
3334

0 commit comments

Comments
 (0)