Class CleanupCampaignStorage

java.lang.Object
org.exoplatform.addons.documents.cleanup.storage.CleanupCampaignStorage

@Component public class CleanupCampaignStorage extends Object
RDBMS storage of cleanup campaigns and their items: the only layer touching the cleanup DAOs, mapping entities to simple domain models. No business logic.
  • Constructor Details

    • CleanupCampaignStorage

      public CleanupCampaignStorage()
  • Method Details

    • createCampaign

      public CleanupCampaign createCampaign(CleanupCampaign campaign)
    • saveCampaign

      public CleanupCampaign saveCampaign(CleanupCampaign campaign)
    • getCampaign

      public CleanupCampaign getCampaign(long campaignId)
    • getCampaignState

      public CleanupCampaignState getCampaignState(long campaignId)
      The CURRENT persisted state of a campaign, bypassing the persistence context (see CleanupCampaignDAO.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

      public List<CleanupCampaign> getCampaigns(org.springframework.data.domain.Pageable pageable)
    • getCampaignsByStates

      public List<CleanupCampaign> getCampaignsByStates(List<CleanupCampaignState> states)
    • 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: checkpointPath is the last processed node path (or a bare scan-root path marking a root not started yet), the ONLY positioning information; checkpointOffset is 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 as updateProgress(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), a state undoing 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 identifier
      name - new campaign name, null to leave the NAME column untouched
      graceDays - new grace period, null to leave GRACE_DAYS untouched
      lockDate - new grace deadline (epoch millis), null to leave LOCK_DATE untouched — only a grace edit on a PUBLISHED campaign rederives it
    • saveCandidates

      public void saveCandidates(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

      public CleanupCampaignItem saveItem(CleanupCampaignItem item)
    • getItem

      public CleanupCampaignItem getItem(long itemId)
    • getItemsTouchedByPath

      public List<CleanupCampaignItem> 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).
    • 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 identifier
      ownerIdentityId - optional owner filter
      state - optional item state filter
      action - optional action filter
      minSize - optional minimal content size filter
      search - 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 column
      pageable - 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 identifier
      state - item state
      lastReclaimableBytes - reclaimable bytes of the last row seen, null to start from the biggest
      lastId - id of the last row seen, breaking size ties
      batchSize - 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 of failureReasons and whose attempt count is still below maxAttemptCount — the retry candidates. Keyset-paged because the requeue mutates the state this very filter matches on.
      Parameters:
      campaignId - campaign identifier
      failureReasons - retryable failure message codes (the Service's allowlist)
      maxAttemptCount - exclusive upper bound on the attempts already spent
      lastId - 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

      public List<CleanupFailureGroup> countFailuresByReason(long campaignId)
      Per-reason counts of a campaign's SKIPPED items, from ONE grouped aggregate query. The retryable flag of each group is left to its default: it is a business rule of the Service layer, not of the query (see CleanupFailureGroup).
      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 as getItems(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) * size rows 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) * size rows 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 appending id 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 identifier
      ownerIdentityIds - owner identities (the user and the spaces they manage)
      search - optional case-insensitive search on the item PATH
      pageable - page, size and sort
      Returns:
      page of items
    • getItemsAfterId

      public List<CleanupCampaignItem> getItemsAfterId(long campaignId, long lastId, int batchSize)
      KEYSET page of a campaign's items in id order, for the CSV export walk — see CleanupCampaignItemDAO.findByCampaignIdAndIdGreaterThanOrderByIdAsc(long, long, org.springframework.data.domain.Pageable) on why this is not an offset Page.
      Parameters:
      campaignId - campaign identifier
      lastId - last item id already exported, 0 to start
      batchSize - maximum number of items to read
      Returns:
      the next items, id ascending, empty when the export is done
    • getItemAggregates

      public Map<Long,CleanupCampaignAggregates> 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.
      Parameters:
      campaignIds - campaign identifiers
      Returns:
      map of campaign id to its item aggregates
    • countItemsByState

      public long countItemsByState(long campaignId, CleanupItemState state)
    • countItemsByOwnersAndState

      public long countItemsByOwnersAndState(long campaignId, List<Long> ownerIdentityIds, CleanupItemState state)
      Chunked like getItemsByOwners(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

      public long sumReclaimableBytesByState(long campaignId, CleanupItemState state)
    • sumReclaimableBytesByOwnersAndState

      public long sumReclaimableBytesByOwnersAndState(long campaignId, List<Long> ownerIdentityIds, CleanupItemState state)
    • sumReclaimedBytes

      public long sumReclaimedBytes(long campaignId)
    • sumReclaimedBytesByOwners

      public long sumReclaimedBytesByOwners(long campaignId, List<Long> ownerIdentityIds)
    • getPersistingItems

      public CleanupComparisonBucket 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).
      Parameters:
      baseCampaignId - base campaign identifier
      otherCampaignId - compared campaign identifier
      Returns:
      count and reclaimable bytes of the bucket
    • getNewItems

      public CleanupComparisonBucket getNewItems(long baseCampaignId, long otherCampaignId)
      Items of the base campaign absent from the other one ('new' bucket of a comparison).
      Parameters:
      baseCampaignId - base campaign identifier
      otherCampaignId - compared campaign identifier
      Returns:
      count and reclaimable bytes of the bucket
    • getGoneItems

      public CleanupComparisonBucket getGoneItems(long baseCampaignId, long otherCampaignId)
      Items of the other campaign absent from the base one ('gone' bucket of a comparison): the very same query as getNewItems(long, long) with the campaigns swapped, so the bytes are read from the other campaign's rows.
      Parameters:
      baseCampaignId - base campaign identifier
      otherCampaignId - compared campaign identifier
      Returns:
      count and reclaimable bytes of the bucket
    • hasItems

      public boolean hasItems(long campaignId)
    • deleteItems

      public void deleteItems(long campaignId)
    • getOrphanItemCampaignIds

      public List<Long> getOrphanItemCampaignIds()
      Campaign ids whose item rows outlived their campaign row — see CleanupCampaignItemDAO#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