Class CleanupJcrStorage

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

@Component public class CleanupJcrStorage extends Object
JCR implementation of CleanupJcrStorage against the collaboration workspace, always through a system session (async cleanup workers carry no user conversation state).

SESSION LIFECYCLE — getSystemSession() opens a BRAND-NEW system session on every call, so every method obtaining one MUST release it in a finally block through logout(Session). The per-item primitives (revalidate / addExemptionMixin / removeExemptionMixin / deleteNode / purgeVersions) are invoked once per campaign item: leaking one session per call would leave tens of thousands of live sessions behind on a large campaign. scanRoot(java.lang.String, java.lang.String, int, org.exoplatform.addons.documents.cleanup.model.CleanupParams, org.exoplatform.addons.documents.cleanup.storage.CleanupJcrStorage.ScanBatchConsumer) holds its session for the WHOLE streamed walk (the NodeIterator is lazy) and logs out only once the iteration is over, never inside the batch loop.

THE ONE EXCEPTION is the observation listener: a JCR listener is bound to the session that registered it, so logging that session out would silently kill the listener. registerObservationListener(BiConsumer) therefore keeps ONE dedicated long-lived session in observationSession, logged out only by unregisterObservationListener() (or by a failed registration).

  • Field Details

  • Constructor Details

    • CleanupJcrStorage

      public CleanupJcrStorage()
  • Method Details

    • listScanUnits

      public List<String> listScanUnits()
      Enumerates the PARTITIONS of a dry-run scan — the subtrees a reader thread each walks on its own — by applying the per-root policy of CleanupConstants: the direct children of a split root (CleanupConstants.SPLIT_SCAN_ROOTS) become units, an unsplit root (CleanupConstants.UNSPLIT_SCAN_ROOTS) is itself one unit. Depth 1 only — read the SPLIT_SCAN_ROOTS comment on what a direct child of /Users really is before touching this.

      A root that does not exist is SKIPPED without failing the scan (/Trash is legitimately absent on a fresh instance), and the paths come back sorted so that re-planning a resumed campaign is deterministic.

      Returns:
      the planned unit paths, sorted
      Throws:
      IllegalStateException - on a JCR failure, so the caller leaves the campaign resumable instead of scanning a partial tree
    • countFiles

      public long countFiles(String rootPath)
      Parameters:
      rootPath - scanned tree root path (e.g. /Users or /Groups/spaces)
      Returns:
      total number of nt:file nodes under the given root
      Throws:
      IllegalStateException - on a JCR failure — the idiom of its siblings scanRoot(java.lang.String, java.lang.String, int, org.exoplatform.addons.documents.cleanup.model.CleanupParams, org.exoplatform.addons.documents.cleanup.storage.CleanupJcrStorage.ScanBatchConsumer) and listScanUnits(), and load-bearing here: a swallowed failure answered 0, which the estimation persisted as a FINAL unit total (the schema separates NULL 'never counted' from 0 'counted, empty'), so a transient hiccup permanently amputated the campaign denominator. Propagating fails the estimation future instead, the run aborts resumable, and TOTAL_COUNT stays NULL for the retry to re-count
    • scanRoot

      public void scanRoot(String rootPath, String resumeAfterPath, int batchSize, CleanupParams params, CleanupJcrStorage.ScanBatchConsumer batchConsumer)
      Scans the path-ordered nt:file nodes under the given root with a SINGLE query, streaming the qualifying cleanup candidates to the consumer in batches of batchSize scanned nodes (the last batch may cover fewer). When resuming, the result is fast-forwarded BY PATH: nodes whose path is not strictly greater than resumeAfterPath are skipped without processing. Unlike a numeric offset, this stays correct when the tree changed during the interruption (no file shifted before the cursor is silently missed), including when the checkpointed node itself no longer exists.
      Parameters:
      rootPath - scanned tree root path
      resumeAfterPath - path of the last node processed before the interruption (resume checkpoint), null or blank for a fresh scan
      batchSize - number of scanned nodes per consumer invocation
      params - campaign parameters snapshot
      batchConsumer - receives each scanned batch; returns false to abort the scan of this root
      Throws:
      IllegalStateException - on a JCR failure, so the caller can leave the scan resumable from its last persisted checkpoint
    • revalidate

      public CleanupRevalidation revalidate(String nodeUuid, CleanupParams params)
      Re-evaluates a node against the campaign criteria, at execution or refresh time. A transient JCR read failure yields a distinct UNKNOWN outcome — never 'spared' nor 'gone' — so a flaky repository can neither permanently spare an item nor let it be deleted on doubt: the node lookup is made directly against the session (not through the null-swallowing helper) to tell 'the node no longer exists' apart from 'the repository failed'.
      Parameters:
      nodeUuid - JCR node identifier
      params - campaign parameters snapshot
      Returns:
      revalidation outcome (gone, exempted, spared, still candidate, or unknown on a JCR read failure)
    • addExemptionMixin

      public CleanupExemptionResult addExemptionMixin(String nodeUuid, String username)
      Adds the exo:cleanupExemption mixin on the node, keeping real modification dates untouched.
      Parameters:
      nodeUuid - JCR node identifier
      username - user who decided to keep the file
      Returns:
      CleanupExemptionResult.ADDED when the node is now exempted, CleanupExemptionResult.NOT_FOUND when it doesn't exist anymore, CleanupExemptionResult.FAILED on a (possibly transient) JCR read or write failure — never NOT_FOUND on doubt, so the user's keep decision is never discarded
    • removeExemptionMixin

      public CleanupExemptionResult removeExemptionMixin(String nodeUuid)
      Removes the exo:cleanupExemption mixin from the node (un-keep), mirroring addExemptionMixin(String, String): a checked-in versionable node is checked out first, and real modification dates stay untouched.
      Parameters:
      nodeUuid - JCR node identifier
      Returns:
      CleanupExemptionResult.ADDED when the node no longer carries the mixin (applied, idempotent), CleanupExemptionResult.NOT_FOUND when it doesn't exist anymore, CleanupExemptionResult.FAILED on a (possibly transient) JCR read or write failure — never NOT_FOUND on doubt, so a flaky repository never marks a still-existing file GONE
    • deleteNode

      public CleanupPurgeResult deleteNode(String nodeUuid)
      Hard-deletes a file node (no trash): removes pointing symlinks first, then the node, then now-empty ancestor folders up to (excluding) the drive root.
      Parameters:
      nodeUuid - JCR node identifier
      Returns:
      purge outcome with reclaimed bytes, GONE only when the node really disappeared, or SKIPPED with a reason (a JCR read failure is SKIPPED, never GONE). A failure happening AFTER the removing session.save() — the empty-ancestor cleanup is the only step left — leaves the item SKIPPED (an administrator must still see the file needed attention) but CARRIES the bytes of the file that is really deleted, exactly as purgeVersions(String, CleanupParams) does for the versions it already removed: dropping them would under-report the campaign's reclaimed total
    • purgeVersions

      public CleanupPurgeResult purgeVersions(String nodeUuid, CleanupParams params)
      Purges the versions of a file selected by CleanupCriterionEvaluator.selectVersionsToRemove(java.util.Map<java.lang.String, java.lang.Long>, java.lang.String, java.lang.String, org.exoplatform.addons.documents.cleanup.model.CleanupParams, long): those older than the campaign period by their OWN creation date, plus — of the versions remaining inside the period — the oldest ones still exceeding maxVersionsPerFile. The root and base (current) versions are never touched, so the file itself always keeps its content.

      The removal set is computed by the SAME pure policy the scan and the revalidation apply, so what an administrator was shown as purgeable is exactly what is removed here.

      Parameters:
      nodeUuid - JCR node identifier
      params - campaign parameters snapshot, carrying the period and the per-file version cap the policy unions
      Returns:
      purge outcome with reclaimed bytes, GONE only when the node really disappeared, or SKIPPED with a reason (a JCR read failure is SKIPPED, never GONE). A version removal failing PARTWAY leaves the item SKIPPED — an administrator must still see the file needs attention — but the bytes already reclaimed by the removals that DID succeed are carried on the result: removeVersion is immediate, so those bytes are really gone and dropping them would under-report the campaign's reclaimed total.
    • registerObservationListener

      public boolean registerObservationListener(BiConsumer<String,String> pathAndEventTypeCallback)
      Registers a JCR observation listener forwarding (nodePath, eventType) pairs for changes under the scanned roots. Idempotent. Unlike every other method here, the session obtained is NOT logged out: the listener is bound to it for its whole lifetime, so it is kept in observationSession and released only by unregisterObservationListener().
      Parameters:
      pathAndEventTypeCallback - callback receiving the event node path and the event type name
      Returns:
      true when the listener is registered (or already was), false when the registration failed (e.g. JCR not ready yet) and may be retried
    • unregisterObservationListener

      public void unregisterObservationListener()
      Unregisters the JCR observation listener, if registered, and logs out the session that owned it. Idempotent.