Class CaldavDeletionService

java.lang.Object
org.exoplatform.caldav.service.CaldavDeletionService

@Service public class CaldavDeletionService extends Object
Deleting a personal calendar, on both sides.

The only irreversible outward-facing action in this design. Removing a collection removes every event in it — including events the user added from their own phone, which eXo never authored and cannot restore. Everything here is arranged around that fact.

  • Field Summary

    Fields
    Modifier and Type
    Field
    Description
    static final String
    The account's calendars could not be listed, so whether the calendar named is a share of the user's cannot be told; nothing was hidden.
    static final String
    No calendar was named to hide.
    static final String
    The calendar named to hide is not one shared with the user (EXO-90239): their own, in whatever state — and hiding one's own calendar is a different act, done by deleting it and keeping the remote one.
    static final String
    Nothing was deleted, on either side.
  • Constructor Summary

    Constructors
    Constructor
    Description
     
  • Method Summary

    Modifier and Type
    Method
    Description
    void
    deleteRemoteCounterpart(long userIdentityId, String username, long calendarId)
    Deletes a personal calendar in eXo and, when eXo created it, on the server too.
    org.exoplatform.caldav.model.CalendarDeletionPlan
    describeDeletion(long userIdentityId, long calendarId)
    What deleting this calendar would also do, so the page can say it before the user confirms.
    void
    freezeOnDisconnect(long userIdentityId, long serverId, String username)
    Freezes every binding of a user, which is what disconnecting an account does.
    static String
    hiddenShareAnchor(String canonicalHref)
    The anchor a hidden share's pair records (EXO-90239): a name-based UUID of the collection path, never an agenda calendar's.
    void
    hideShare(long userIdentityId, String username, String calendarId)
    Hides a calendar somebody shared with the user (EXO-90239).
    void
    keepRemoteCounterpart(long userIdentityId, long calendarId)
    Deletes the calendar in eXo and leaves the remote collection standing, because the user asked for exactly that.
    List<org.exoplatform.caldav.model.HiddenCalendar>
    listHidden(long userIdentityId, String username)
    The calendars this user has hidden on this account.
    List<org.exoplatform.caldav.model.CalendarSyncState>
    listSyncStates(long userIdentityId, String username)
    What each of this user's calendars is doing, for the ones worth telling them about.
    void
    showAgain(long userIdentityId, long pairId, String username)
    Shows a hidden calendar again.
    void
    thawOnConnect(long userIdentityId, long serverId)
    Puts back to work every binding a disconnect froze.

    Methods inherited from class java.lang.Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Field Details

    • NOTHING_DELETED

      public static final String NOTHING_DELETED
      Nothing was deleted, on either side.
      See Also:
    • NOT_A_SHARE

      public static final String NOT_A_SHARE
      The calendar named to hide is not one shared with the user (EXO-90239): their own, in whatever state — and hiding one's own calendar is a different act, done by deleting it and keeping the remote one.
      See Also:
    • CALENDAR_REQUIRED

      public static final String CALENDAR_REQUIRED
      No calendar was named to hide.
      See Also:
    • ACCOUNT_UNAVAILABLE

      public static final String ACCOUNT_UNAVAILABLE
      The account's calendars could not be listed, so whether the calendar named is a share of the user's cannot be told; nothing was hidden.
      See Also:
  • Constructor Details

    • CaldavDeletionService

      public CaldavDeletionService()
  • Method Details

    • deleteRemoteCounterpart

      public void deleteRemoteCounterpart(long userIdentityId, String username, long calendarId)
      Deletes a personal calendar in eXo and, when eXo created it, on the server too.

      Remote first, and the order is the whole design. The failable step runs while nothing has happened yet, so a server that refuses or cannot be reached leaves both sides exactly as they were. The reverse order can strand a collection on the server after the record that knew about it is gone — an orphan nothing will ever find again, holding the user's events.

      Success is confirmed by the collection's absence from a fresh listing, never by a status code. This migration has twice met a server that answers success while doing nothing, and a deletion that reports done while the collection still stands would take the local calendar with it.

      The local half is not done here. Agenda owns the calendar and deletes it itself, once this returns without throwing — which is exactly what makes the ordering work: the failable step runs first, and a refusal here stops agenda before it has touched anything.

      Parameters:
      userIdentityId - identity of the user
      username - the eXo login the credentials provider resolves the account from
      calendarId - the eXo calendar being deleted
      Throws:
      CaldavPushException - when the remote deletion could not be carried out; nothing has been deleted on either side
    • keepRemoteCounterpart

      public void keepRemoteCounterpart(long userIdentityId, long calendarId)
      Deletes the calendar in eXo and leaves the remote collection standing, because the user asked for exactly that.

      The escape hatch from the atomic rule, and it exists so that divergence between the two sides is only ever chosen, named and recorded — never a side effect of a failed deletion. The pair is kept as an CalendarSyncStatus.EXO_ORPHANED tombstone so the settings can say that a collection of eXo's making is still out there.

      Parameters:
      userIdentityId - identity of the user
      calendarId - the eXo calendar being deleted
    • describeDeletion

      public org.exoplatform.caldav.model.CalendarDeletionPlan describeDeletion(long userIdentityId, long calendarId)
      What deleting this calendar would also do, so the page can say it before the user confirms.

      The page cannot work this out for itself: it does not know whether eXo created the remote collection, nor which server holds it. Both decide what the confirmation must warn about — and whether it must warn at all.

      Parameters:
      userIdentityId - identity of the user
      calendarId - the eXo calendar in question
      Returns:
      what is bound to it, or a plan claiming nothing
    • thawOnConnect

      public void thawOnConnect(long userIdentityId, long serverId)
      Puts back to work every binding a disconnect froze.

      Disconnecting pauses the bindings of the calendars eXo pushed out, so that reconnecting the same account finds its collections again instead of creating a second set beside them. Reconnecting is the other half of that bargain: the account is usable again, so its bindings are too.

      Left paused they are repaired only incidentally — by the next sweep re-ensuring each collection, a second or so later. In the meantime the account is connected and the user's own calendars report themselves as failing, which a UI reading the states has every reason to believe and show.

      A pause set because credentials were refused is thawed here as well, and deliberately: someone who has just entered a password is asking for precisely that retry.

      Parameters:
      userIdentityId - identity of the user
      serverId - the declared server registration
    • freezeOnDisconnect

      public void freezeOnDisconnect(long userIdentityId, long serverId, String username)
      Freezes every binding of a user, which is what disconnecting an account does.

      Nothing on the server is ever touched. A user unlinking their account is saying "stop syncing", not "destroy what is on my server".

      Nothing in eXo is destroyed either, and that is the change (EXO-89800). A calendar eXo materialised from a collection of the account used to go with the account: its eXo calendar was deleted, and with it every event in it, its object mappings and its binding. Reconnecting the same account then found no binding for the collection, materialised it a second time, and imported every event again under new identifiers — observed live as the same BlueMind collection materialised at 23:28 and again at 08:37 the next morning, as two different eXo calendars.

      That deletion could not be undone and was not the user's to lose. A materialised calendar is an ordinary agenda calendar: the user adds events to it, and the connector pushes those back to the account, so it holds work of theirs that exists nowhere else. Deleting it on "stop syncing" also broke every reference to the events it held — an activity, a notification, a link — since the re-imported copies carry new identifiers.

      So every binding is now treated the way a pushed calendar already was, and for the reason its own comment gave: reconnecting the same account must find its collection again instead of creating a second one beside it. The binding is paused, the calendar stays, and thawOnConnect(long, long) puts it back to work.

      A paused binding stops importing, which is what "no longer updating" means; listSyncStates(long, String) reports the state so the settings can say so. And reconnecting a different account on the same server does not resurrect the old collections: their hrefs are absent from the new account's listing, so the first pass marks them CalendarSyncStatus.REMOTE_GONE and they stop receiving events — visibly, rather than by having been destroyed in advance.

      Parameters:
      userIdentityId - identity of the user
      serverId - the declared server registration
      username - the user's login. No longer needed to delete anything on their behalf, and kept because it is still what tells the caller the account can be identified at all — CaldavConnectorServiceImpl.deleteCaldavSetting(long, String) only freezes when it has one — and because a support trace of a disconnect is worth the login it happened under
    • hideShare

      public void hideShare(long userIdentityId, String username, String calendarId) throws org.exoplatform.commons.exception.ObjectNotFoundException
      Hides a calendar somebody shared with the user (EXO-90239).

      A share is never materialised, so until now it had no pair — and being unbound is exactly what listed it under the remote calendars and served its events. The only way to stop seeing it was the row checkbox, which lasted until the next reload. Hiding records the choice as a pair in CalendarSyncStatus.HIDDEN_SHARE: bound, the collection leaves the list and its events stop being served (CaldavReadService), and the sweep never considers it (CaldavSyncService#isAlreadyOurs). No calendar is created, nothing is written to the server, and nothing of the owner's is touched.

      The id is trusted no further than the user's own listing. It is a collection href that travelled through a browser, and what a changed one would name is another collection on the same account. So it is matched, canonically, against the calendars the list would answer this user now — the same listing, the same classification — and refused unless that listing holds it as a share. A calendar of the user's own is refused in words: hiding one's own is done by deleting it and keeping the remote copy, which is a different act with a different warning. The user's pairs are asked first, cheaply: a collection already bound is either hidden already — nothing to do, and the answer is the same as if this call had done it — or an eXo calendar of the user's own, and the server is not asked about either.

      Parameters:
      userIdentityId - identity of the user
      username - the eXo login the credentials provider resolves the account from
      calendarId - the calendar's identity, exactly as the calendar list answered it — the collection href
      Throws:
      org.exoplatform.commons.exception.ObjectNotFoundException - when the user's current listing holds no such calendar
      IllegalArgumentException - with CALENDAR_REQUIRED when no calendar is named, and with NOT_A_SHARE when the one named is the user's own
      CaldavPushException - with NOT_CONNECTED when no account is connected, and with ACCOUNT_UNAVAILABLE when the account could not be listed; nothing was hidden either way
    • hiddenShareAnchor

      public static String hiddenShareAnchor(String canonicalHref)
      The anchor a hidden share's pair records (EXO-90239): a name-based UUID of the collection path, never an agenda calendar's.

      A hidden share has no eXo calendar, so it has no anchor of its own; the column cannot simply stay null all the same. The pair table's one unique index is (USER_IDENTITY_ID, SERVER_ID, LOCAL_CALENDAR_SYNC_UID), and while MySQL, PostgreSQL and HSQLDB leave a row with a null key column outside it, Oracle enforces a composite unique key over the columns that are not null and SQL Server treats nulls as equal: there, a second hidden share of one user on one server — or a hidden share beside the mirror pair, the other null anchor — would be refused. A value derived from the path is unique per collection on every engine, and it makes that index enforce what the service means besides: one record per hidden calendar, whatever two requests race.

      It cannot name a calendar by accident. Agenda mints its anchors with UUID.randomUUID(), version 4, and this is version 3, so the two never meet; and every consumer that reads an anchor as a calendar's either looks up agenda's own anchors, selects SyncOrigin.EXO pairs, or selects CalendarSyncStatus.ACTIVE ones.

      Parameters:
      canonicalHref - the collection path, canonical
      Returns:
      a 36-character anchor, the same for the same path
    • listHidden

      public List<org.exoplatform.caldav.model.HiddenCalendar> listHidden(long userIdentityId, String username)
      The calendars this user has hidden on this account.

      A tombstone is what makes a deletion stick: it keeps a collection from being materialised again, and since the shim stopped serving bound collections it keeps it off the screen entirely. That is what the user asked for — and it leaves them with no way back, which is why this exists. A hidden share (CalendarSyncStatus.HIDDEN_SHARE, EXO-90239) is listed beside the tombstones for the same reason, told apart by shared, and named with whoever shared it — the owner the calendar list would name, resolved through the same service, so the drawer says "Shared by Alice" about the very calendar the list said that of.

      The name comes from the server, read now rather than stored: a calendar renamed in the user's own client since they hid it should be offered back under the name they would recognise today. A collection the server no longer has is left out — offering to show something that is gone would be a promise nothing can keep. For a share that also means the owner withdrew it; the sweep drops its record on its next pass (CaldavSyncService#forgetRevokedShares), and this listing does not, because a listing is read on every visit to the settings and must not write.

      Parameters:
      userIdentityId - identity of the user
      username - the eXo login the credentials provider resolves the account from
      Returns:
      what can be shown again, empty when nothing is hidden
    • showAgain

      public void showAgain(long userIdentityId, long pairId, String username) throws IllegalAccessException, org.exoplatform.commons.exception.ObjectNotFoundException
      Shows a hidden calendar again.

      Dropping the tombstone is all it takes: the next sync finds a collection with no binding and materialises it afresh. Deliberately a new calendar rather than a resurrection — the one the user deleted is gone, and pretending otherwise would promise back events that agenda moved to their default calendar at deletion time.

      For a hidden share (EXO-90239) dropping the pair is all there is: a share is never materialised, so no synchronisation would bring anything back, and the collection is back under the remote calendars on the very next listing because it is unbound again. Synchronising here would spend a full pass of the account to change nothing.

      Parameters:
      userIdentityId - identity of the user
      pairId - the hidden calendar to lift — a tombstone or a hidden share
      username - the user's login, which the synchronisation run here needs because agenda's ACL reads it rather than the identity id
      Throws:
      IllegalAccessException - when the hidden calendar is not this user's
      org.exoplatform.commons.exception.ObjectNotFoundException - when there is no such hidden calendar, tombstone or hidden share
    • listSyncStates

      public List<org.exoplatform.caldav.model.CalendarSyncState> listSyncStates(long userIdentityId, String username)
      What each of this user's calendars is doing, for the ones worth telling them about.

      The engine has always known this per binding and kept it in the database and the logs. A user seeing their agenda has no way to tell a calendar that is synchronising from one whose server refused it — both simply sit there — so this surfaces the states where something they might do would change the outcome, and nothing else.

      Names come from the eXo calendar for a binding that has one, and from the server for the ones that do not — a collection eXo was refused permission to create has no eXo calendar to name it by.

      Parameters:
      userIdentityId - whose calendars
      username - the user's login, which agenda's ACL needs to read their calendars
      Returns:
      the states worth showing, empty when everything is well