Class CleanupCampaignStorage
-
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptioncountFailuresByReason(long campaignId) Per-reason counts of a campaign's SKIPPED items, from ONE grouped aggregate query.longcountItemsByOwnersAndState(long campaignId, List<Long> ownerIdentityIds, CleanupItemState state) Chunked likegetItemsByOwners(long, java.util.List<java.lang.Long>, java.lang.String, org.springframework.data.domain.Pageable), but merging is trivial and EXACT here: the owner sets are disjoint, so the per-chunk counts simply add up.longcountItemsByState(long campaignId, CleanupItemState state) createCampaign(CleanupCampaign campaign) voiddeleteCampaign(long campaignId) Drops the campaign row itself — there is no cascade, by design: a cascade would make a campaign row deletable without anyone deciding what happens to the report and the archived CSV hanging off it.voiddeleteItems(long campaignId) getCampaign(long campaignId) getCampaigns(org.springframework.data.domain.Pageable pageable) getCampaignsByStates(List<CleanupCampaignState> states) getCampaignState(long campaignId) The CURRENT persisted state of a campaign, bypassing the persistence context (seeCleanupCampaignDAO.findStateById(long)): what the abort polls and the transition guard must read, an entity load answering a possibly hours-old snapshot inside the workers' long-lived transactions.getGoneItems(long baseCampaignId, long otherCampaignId) Items of the other campaign absent from the base one ('gone' bucket of a comparison): the very same query asgetNewItems(long, long)with the campaigns swapped, so the bytes are read from the other campaign's rows.getItem(long itemId) getItemAggregates(List<Long> campaignIds) Item aggregates of a whole campaigns list, computed by ONE grouped query (see the DAO) instead of per-campaign aggregate queries: a campaign absent from the returned map simply has no item rows anymore.org.springframework.data.domain.Page<CleanupCampaignItem> getItems(long campaignId, Long ownerIdentityId, CleanupItemState state, CleanupAction action, Long minSize, String search, org.springframework.data.domain.Pageable pageable) Campaign items matching the provided filters, any of them nullable.getItemsAfterId(long campaignId, long lastId, int batchSize) KEYSET page of a campaign's items in id order, for the CSV export walk — seeCleanupCampaignItemDAO.findByCampaignIdAndIdGreaterThanOrderByIdAsc(long, long, org.springframework.data.domain.Pageable)on why this is not an offsetPage.org.springframework.data.domain.Page<CleanupCampaignItem> getItemsByOwners(long campaignId, List<Long> ownerIdentityIds, String search, org.springframework.data.domain.Pageable pageable) Items of the campaign owned by the given identities, optionally narrowed by the same path search asgetItems(long, java.lang.Long, org.exoplatform.addons.documents.cleanup.constant.CleanupItemState, org.exoplatform.addons.documents.cleanup.constant.CleanupAction, java.lang.Long, java.lang.String, org.springframework.data.domain.Pageable).org.springframework.data.domain.Page<CleanupCampaignItem> getItemsByState(long campaignId, CleanupItemState state, org.springframework.data.domain.Pageable pageable) getItemsByStateBiggestFirst(long campaignId, CleanupItemState state, Long lastReclaimableBytes, long lastId, int batchSize) KEYSET page of the items of a campaign in a given state, BIGGEST FIRST — see the DAO javadoc for why the cursor is a(reclaimableBytes, id)pair rather than an id, and why that still cannot revisit a row.getItemsTouchedByPath(long campaignId, String eventPath) Items touched by a JCR event path: the event node itself, its ancestors (exact match against the ancestor chain, computed here) and its descendants (escaped prefix LIKE, so a deleted/moved folder event matches the candidate files below it).getNewItems(long baseCampaignId, long otherCampaignId) Items of the base campaign absent from the other one ('new' bucket of a comparison).Campaign ids whose item rows outlived their campaign row — seeCleanupCampaignItemDAO#findOrphanCampaignIds.getPersistingItems(long baseCampaignId, long otherCampaignId) Items of the base campaign whose node uuid is ALSO a candidate of the other campaign ('persisting' bucket of a comparison).getRetryableFailures(long campaignId, Set<String> failureReasons, long maxAttemptCount, long lastId, int batchSize) KEYSET page of the SKIPPED items of a campaign whose failure reason is one offailureReasonsand whose attempt count is still belowmaxAttemptCount— the retry candidates.booleanhasItems(long campaignId) saveCampaign(CleanupCampaign campaign) voidsaveCandidates(long campaignId, List<CleanupCandidate> candidates) Saves scan candidates as campaign items, ignoring node uuids already recorded for the campaign (scan resume may replay a batch): one bulk existence query plus one saveAll per batch, backed by the UK_DOC_CLEANUP_ITEM_NODE unique constraint.saveItem(CleanupCampaignItem item) longsumReclaimableBytesByOwnersAndState(long campaignId, List<Long> ownerIdentityIds, CleanupItemState state) Chunked and additively merged, seecountItemsByOwnersAndState(long, java.util.List<java.lang.Long>, org.exoplatform.addons.documents.cleanup.constant.CleanupItemState).longsumReclaimableBytesByState(long campaignId, CleanupItemState state) longsumReclaimedBytes(long campaignId) longsumReclaimedBytesByOwners(long campaignId, List<Long> ownerIdentityIds) Chunked and additively merged, seecountItemsByOwnersAndState(long, java.util.List<java.lang.Long>, org.exoplatform.addons.documents.cleanup.constant.CleanupItemState).voidupdateEditableAttributes(long campaignId, String name, Integer graceDays, Long lockDate) TARGETED write of the attributes an administrator PATCH owns: the NAME, and the grace period together with the grace DEADLINE derived from it.voidupdateProgress(long campaignId, long totalCount, long processedCount, long etaSeconds, String checkpointPath, long checkpointOffset) Persists the scan/execution progress counters and the scan resume checkpoint:checkpointPathis the last processed node path (or a bare scan-root path marking a root not started yet), the ONLY positioning information;checkpointOffsetis the scanned-in-root count, kept for progress/ETA display only.
-
Constructor Details
-
CleanupCampaignStorage
public CleanupCampaignStorage()
-
-
Method Details
-
createCampaign
-
saveCampaign
-
getCampaign
-
getCampaignState
The CURRENT persisted state of a campaign, bypassing the persistence context (seeCleanupCampaignDAO.findStateById(long)): what the abort polls and the transition guard must read, an entity load answering a possibly hours-old snapshot inside the workers' long-lived transactions.- Parameters:
campaignId- campaign identifier- Returns:
- the persisted state, null when the campaign row is gone
-
getCampaigns
-
getCampaignsByStates
-
updateProgress
public void updateProgress(long campaignId, long totalCount, long processedCount, long etaSeconds, String checkpointPath, long checkpointOffset) Persists the scan/execution progress counters and the scan resume checkpoint:checkpointPathis the last processed node path (or a bare scan-root path marking a root not started yet), the ONLY positioning information;checkpointOffsetis the scanned-in-root count, kept for progress/ETA display only. -
updateEditableAttributes
public void updateEditableAttributes(long campaignId, String name, Integer graceDays, Long lockDate) TARGETED write of the attributes an administrator PATCH owns: the NAME, and the grace period together with the grace DEADLINE derived from it. The row is re-read inside the transaction and ONLY those columns are set — the very same idiom asupdateProgress(long, long, long, long, java.lang.String, long), for the very same reason.Deliberately NOT a whole-row
saveCampaign(CleanupCampaign): this row is written on a SCHEDULE by two other writers — the progress updates of the scan and purge workers, and the grace-deadline cron — and the campaign table carries no@Version. A read-modify-write of every column would push the snapshot read before the edit back over whatever they wrote in between: a rewound scan checkpoint (hours of re-walking), astateundoing the lifecycle's PUBLISHED to LOCKED transition, or a nulled ARCHIVE_FILE_ID orphaning the CSV report in the file service for good.Every argument is nullable and means 'leave that column ALONE'. Which of them an edit actually changed is the Service's business, not this layer's.
- Parameters:
campaignId- campaign identifiername- new campaign name, null to leave the NAME column untouchedgraceDays- new grace period, null to leave GRACE_DAYS untouchedlockDate- new grace deadline (epoch millis), null to leave LOCK_DATE untouched — only a grace edit on a PUBLISHED campaign rederives it
-
saveCandidates
Saves scan candidates as campaign items, ignoring node uuids already recorded for the campaign (scan resume may replay a batch): one bulk existence query plus one saveAll per batch, backed by the UK_DOC_CLEANUP_ITEM_NODE unique constraint. -
saveItem
-
getItem
-
getItemsTouchedByPath
Items touched by a JCR event path: the event node itself, its ancestors (exact match against the ancestor chain, computed here) and its descendants (escaped prefix LIKE, so a deleted/moved folder event matches the candidate files below it). -
getItems
public org.springframework.data.domain.Page<CleanupCampaignItem> getItems(long campaignId, Long ownerIdentityId, CleanupItemState state, CleanupAction action, Long minSize, String search, org.springframework.data.domain.Pageable pageable) Campaign items matching the provided filters, any of them nullable.- Parameters:
campaignId- campaign identifierownerIdentityId- optional owner filterstate- optional item state filteraction- optional action filterminSize- optional minimal content size filtersearch- optional case-insensitive search on the item PATH — which covers the file name (the path's last segment) as well as the folders above it, the item table having no name columnpageable- page, size and sort- Returns:
- page of items
-
getItemsByState
public org.springframework.data.domain.Page<CleanupCampaignItem> getItemsByState(long campaignId, CleanupItemState state, org.springframework.data.domain.Pageable pageable) -
getItemsByStateBiggestFirst
public List<CleanupCampaignItem> getItemsByStateBiggestFirst(long campaignId, CleanupItemState state, Long lastReclaimableBytes, long lastId, int batchSize) KEYSET page of the items of a campaign in a given state, BIGGEST FIRST — see the DAO javadoc for why the cursor is a(reclaimableBytes, id)pair rather than an id, and why that still cannot revisit a row.- Parameters:
campaignId- campaign identifierstate- item statelastReclaimableBytes- reclaimable bytes of the last row seen, null to start from the biggestlastId- id of the last row seen, breaking size tiesbatchSize- maximum number of items to read- Returns:
- the next items, biggest first, empty when the state is exhausted
-
getRetryableFailures
public List<CleanupCampaignItem> getRetryableFailures(long campaignId, Set<String> failureReasons, long maxAttemptCount, long lastId, int batchSize) KEYSET page of the SKIPPED items of a campaign whose failure reason is one offailureReasonsand whose attempt count is still belowmaxAttemptCount— the retry candidates. Keyset-paged because the requeue mutates the state this very filter matches on.- Parameters:
campaignId- campaign identifierfailureReasons- retryable failure message codes (the Service's allowlist)maxAttemptCount- exclusive upper bound on the attempts already spentlastId- last id already seen (0 to start from the beginning)batchSize- maximum number of items to read- Returns:
- the next retryable items, id ascending
-
countFailuresByReason
Per-reason counts of a campaign's SKIPPED items, from ONE grouped aggregate query. Theretryableflag of each group is left to its default: it is a business rule of the Service layer, not of the query (seeCleanupFailureGroup).- Parameters:
campaignId- campaign identifier- Returns:
- one group per distinct failure reason, empty when the campaign has no failed item left (the retention job purged its item rows)
-
getItemsByOwners
public org.springframework.data.domain.Page<CleanupCampaignItem> getItemsByOwners(long campaignId, List<Long> ownerIdentityIds, String search, org.springframework.data.domain.Pageable pageable) Items of the campaign owned by the given identities, optionally narrowed by the same path search asgetItems(long, java.lang.Long, org.exoplatform.addons.documents.cleanup.constant.CleanupItemState, org.exoplatform.addons.documents.cleanup.constant.CleanupAction, java.lang.Long, java.lang.String, org.springframework.data.domain.Pageable).The owner list is CHUNKED (see
MAX_IN_CLAUSE_SIZE), so a user managing a thousand spaces doesn't get an ORA-01795 on their own review page. With a single chunk — the overwhelmingly common case — the DAO page is returned untouched. With several, each chunk is queried with the SAME total ordering as requested (querySort(Sort)-translated for both paths alike, so the computed reclaimable key is ordered by the database and not silently dropped),(page + 1) * sizerows are taken from each, they are merged with a comparator mirroring that ordering and the requested page is sliced out of the merge.TRADE-OFF of the multi-chunk path: taking
(page + 1) * sizerows per chunk is what makes the merge exact (the requested page can, at worst, be filled entirely from one chunk), so deep paging costs more rows per chunk — bounded, since the review UI pages at 20/50/100. The requested ordering must be TOTAL for the slice to be stable; the REST layer guarantees that by appendingid ASC(the primary key) to every ordering. The total element count is the exact sum of the chunks' counts, the owner sets being disjoint by construction.- Parameters:
campaignId- campaign identifierownerIdentityIds- owner identities (the user and the spaces they manage)search- optional case-insensitive search on the item PATHpageable- page, size and sort- Returns:
- page of items
-
getItemsAfterId
KEYSET page of a campaign's items in id order, for the CSV export walk — seeCleanupCampaignItemDAO.findByCampaignIdAndIdGreaterThanOrderByIdAsc(long, long, org.springframework.data.domain.Pageable)on why this is not an offsetPage.- Parameters:
campaignId- campaign identifierlastId- last item id already exported, 0 to startbatchSize- maximum number of items to read- Returns:
- the next items, id ascending, empty when the export is done
-
getItemAggregates
Item aggregates of a whole campaigns list, computed by ONE grouped query (see the DAO) instead of per-campaign aggregate queries: a campaign absent from the returned map simply has no item rows anymore.- Parameters:
campaignIds- campaign identifiers- Returns:
- map of campaign id to its item aggregates
-
countItemsByState
-
countItemsByOwnersAndState
public long countItemsByOwnersAndState(long campaignId, List<Long> ownerIdentityIds, CleanupItemState state) Chunked likegetItemsByOwners(long, java.util.List<java.lang.Long>, java.lang.String, org.springframework.data.domain.Pageable), but merging is trivial and EXACT here: the owner sets are disjoint, so the per-chunk counts simply add up. -
sumReclaimableBytesByState
-
sumReclaimableBytesByOwnersAndState
public long sumReclaimableBytesByOwnersAndState(long campaignId, List<Long> ownerIdentityIds, CleanupItemState state) Chunked and additively merged, seecountItemsByOwnersAndState(long, java.util.List<java.lang.Long>, org.exoplatform.addons.documents.cleanup.constant.CleanupItemState). -
sumReclaimedBytes
public long sumReclaimedBytes(long campaignId) -
sumReclaimedBytesByOwners
Chunked and additively merged, seecountItemsByOwnersAndState(long, java.util.List<java.lang.Long>, org.exoplatform.addons.documents.cleanup.constant.CleanupItemState). -
getPersistingItems
Items of the base campaign whose node uuid is ALSO a candidate of the other campaign ('persisting' bucket of a comparison).- Parameters:
baseCampaignId- base campaign identifierotherCampaignId- compared campaign identifier- Returns:
- count and reclaimable bytes of the bucket
-
getNewItems
Items of the base campaign absent from the other one ('new' bucket of a comparison).- Parameters:
baseCampaignId- base campaign identifierotherCampaignId- compared campaign identifier- Returns:
- count and reclaimable bytes of the bucket
-
getGoneItems
Items of the other campaign absent from the base one ('gone' bucket of a comparison): the very same query asgetNewItems(long, long)with the campaigns swapped, so the bytes are read from the other campaign's rows.- Parameters:
baseCampaignId- base campaign identifierotherCampaignId- compared campaign identifier- Returns:
- count and reclaimable bytes of the bucket
-
hasItems
public boolean hasItems(long campaignId) -
deleteItems
public void deleteItems(long campaignId) -
getOrphanItemCampaignIds
Campaign ids whose item rows outlived their campaign row — seeCleanupCampaignItemDAO#findOrphanCampaignIds.- Returns:
- the orphaned campaign ids, empty when there is nothing to sweep
-
deleteCampaign
public void deleteCampaign(long campaignId) Drops the campaign row itself — there is no cascade, by design: a cascade would make a campaign row deletable without anyone deciding what happens to the report and the archived CSV hanging off it.On the DELETE path this row goes FIRST and its item/unit rows follow asynchronously (see
CleanupCampaignService#deleteCampaign): dropping the row is what makes the campaign instantly unreachable, and no query can reach an item row whose campaign is gone. The retention path is the other way round — it drops items and KEEPS the row, the archived CSV being the report from then on.- Parameters:
campaignId- campaign identifier
-