@@ -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 ();
0 commit comments