Class CaldavOutboundService
This is the first code that creates collections in the customer's own calendar space. Until now eXo only ever wrote into a collection it had made for itself, and everything here is shaped by that difference.
The path is the identity of a binding and the display name is data carried along it: every pass that settles on a collection — listed, answered directly, or just created — also makes sure the collection reads under the calendar's current eXo name, and confirms that by reading the name back. Before EXO-89528's fix the name was written once, inside MKCALENDAR, and never again, so a calendar renamed in eXo kept its first name on the server for good — and re-connecting the account could not help, because the binding is recovered by path and found the same collection under the same stale name.
-
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final StringEvery collection this service creates is named from this prefix. -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionstatic StringThe calendar anchor a collection's slug carries, when eXo minted it.List<org.exoplatform.caldav.model.CalendarSync> bindPersonalCalendars(long userIdentityId, String username) Binds every personal calendar of a user to a collection on their server, creating what does not exist yet.static StringcontainerUidOf(String href) The container uid a collection is named by: its last path segment.exportingUserOf(long serverId, String href) Which user of this deployment exported the calendar a collection stands for — the who form ofisMintedByThisDeployment(long, java.lang.String).static booleanisExoCreated(String href) Whether an eXo created this collection, judged from its path alone — this deployment or any other.booleanisHeldByThisDeployment(long serverId, String containerUid) Whether a user of this deployment, anyone, holds a calendar for the container a collection stands for — under whatever home the server listed it to them (EXO-90347).booleanisMintedByThisDeployment(long serverId, String href) Whether this deployment is the one that created this collection: its slug is eXo's and a user here holds the calendar it stands for.ownershipOf(long serverId, String principal, List<org.exoplatform.caldav.model.CalendarSync> usersPairs, CalendarCollection collection) Whose a listed collection is, to the user whose account listed it.ownershipOf(long serverId, String principal, List<org.exoplatform.caldav.model.CalendarSync> usersPairs, CalendarCollection collection, AccountCalendarOwners owners) Whose a listed collection is, with the server's own word on its owners heard — the form the sweep and the calendar list ask (EXO-90347).
-
Field Details
-
COLLECTION_PREFIX
Every collection this service creates is named from this prefix.- See Also:
-
-
Constructor Details
-
CaldavOutboundService
public CaldavOutboundService()
-
-
Method Details
-
bindPersonalCalendars
public List<org.exoplatform.caldav.model.CalendarSync> bindPersonalCalendars(long userIdentityId, String username) Binds every personal calendar of a user to a collection on their server, creating what does not exist yet.This is the first step that talks to the server in both sequences that reach it — a connection and a synchronisation pass — so what it does with a failure decides how many credential-bearing requests the whole sequence makes. Two failures are re-thrown rather than absorbed, because they are properties of the server and are settled after one attempt:
CalDavAuthenticationException(a person must act) andCalDavUnreachableException(nothing is there). Letting the caller continue past either of them buys nothing and costs four more authenticated requests against a server that may be counting them (EXO-89806).Anything else stays absorbed exactly as before: a home this account cannot be read out of, an answer that is not DAV XML, a quirk on one account — those say nothing about the rest of the pass, whose later steps ask the server different questions and may well be answered.
Being that first step, it is also where the account's identity is recorded (EXO-90243): the discovery answers the
current-user-principalon its way to the home, so keeping it costs no request, and it is kept on connect and at the start of every pass alike. Recorded as soon as the discovery answers, before the listing, because who the account is does not depend on whether its calendars can be listed.- Parameters:
userIdentityId- identity of the userusername- the user's login, which agenda's ACL reads- Returns:
- the pairs, bound or refused, one per personal calendar
- Throws:
CalDavAuthenticationException- when the server refused the stored credentialsCalDavUnreachableException- when the server could not be reached
-
isExoCreated
Whether an eXo created this collection, judged from its path alone — this deployment or any other.The path says which connector minted a collection, not which deployment: every eXo derives the same
COLLECTION_PREFIXslug, so a collection under it was made by this connector for one of some user's calendars, on this instance or on another one writing into the same account. That is as far as the path can see, and it used to be the whole test (EXO-89530, EXO-90190): a pair check is scoped to one user, while the collections in a shared account were made by any of them, so one user's outbound copy looked, to another user's pair, like an ordinary remote calendar to materialise, read and write back into — observed live as one user'sexo-cal-946eec40…coming back as another user's calendar 23.The sweep and the push no longer stop at this answer. What they need to know is whether the calendar behind the collection exists here, which is
isMintedByThisDeployment(long, String); a collection another deployment minted is an ordinary remote calendar to them (EXO-90226). This remains the cheap, stateless form for the places that only need to tell eXo-shaped paths from the rest.- Parameters:
href- the collection path, canonical or not; only its last segment is read- Returns:
- true when the path is one an eXo derives for a user's own calendar
-
anchorOf
The calendar anchor a collection's slug carries, when eXo minted it.The inverse of
collectionHref(String, String): the slug is the prefix followed by agenda's calendar sync uid, and nothing else. Read from the last segment alone, so a server that republishes the collection under another parent — BlueMind lists eXo's collections under a path other than the one they were created at — still yields the anchor.- Parameters:
href- the collection path, canonical or not- Returns:
- the anchor, or null when the slug is not one eXo mints, or carries nothing after the prefix
-
isMintedByThisDeployment
Whether this deployment is the one that created this collection: its slug is eXo's and a user here holds the calendar it stands for.The distinction a path prefix cannot make (EXO-90226). Two users on one deployment sharing an account: the other user's calendar already exists inside eXo, so importing its collection would duplicate what eXo knows natively — skip it. Two separate deployments sharing an account: the other instance's calendar exists nowhere here, there is nothing to duplicate, and importing it is exactly what any second CalDAV client connected to the account does. Both cases wear the same prefix; only the pair table tells them apart, and since EXO-90190 it can be asked account-wide rather than for one user.
Ownership is asked two ways, and either answers. By the anchor the slug carries first: the server may list the collection under a parent other than the one eXo created it at — BlueMind republishes them under
…/publish/…— and the slug survives that. By the recorded path second: the same server has also reported a collection under a slug other than the one it was created with (EXO-89590 — prefix kept, suffix replaced), and then the slug carries no anchor anyone here holds, while an EXO pair recorded at that path still says the collection is this deployment's. The path arm only ever adds skips, so it cannot reintroduce what made the path wrong as the sole key: a colleague's collection republished under another parent still answers by its anchor. What neither arm answers is a collection republished under another slug whose pair still records the created path; only a captured BlueMind listing can say whether that shape occurs.The failure direction depends on which side kept its state. A deployment cloned from another's database answers "ours" for the other's collections, which is the skip that held before this question existed. A deployment whose pair rows are gone while the collections are not — wiped and re-seeded, restored to a point before the pairs were written, re-pointed at an account it used to serve — answers "not ours" for its own former collections and adopts them. Where the restore kept agenda's calendars and their sync uids, that heals within one pass:
bindPersonalCalendars(long, java.lang.String)runs before materialisation and re-records the binding by the derived path, so materialisation finds the anchor again. Only a full wipe adopts, and what it adopts is bounded — one extra REMOTE-bound calendar per pre-wipe calendar, named from the collection, holding the old events; no anchor collision, since materialisation records the new anchor; no name-collision failure, since agenda accepts two calendars of one name; and nothing pushed back out, since a REMOTE pair stopsbindbefore it creates anything. Visible and reversible by the user, which is the property the adoption default rests on.The sweep asks this before reading through a binding, the push before writing through one, and
ownershipOf(long, java.lang.String, java.util.List<org.exoplatform.caldav.model.CalendarSync>, org.exoplatform.caldav.client.CalendarCollection)— which the sweep and the read-through both classify a listed collection with — asks it for a collection none of the user's own pairs recognise. One definition, so the answers cannot drift. A path outside the outbound prefix asks nothing: no eXo minted it, and keeping the database out of that case is what keeps the question cheap on a listing that is mostly the user's own calendars.- Parameters:
serverId- the declared server registrationhref- the collection path, canonical or not- Returns:
- true when a calendar of this deployment stands behind it
-
isHeldByThisDeployment
Whether a user of this deployment, anyone, holds a calendar for the container a collection stands for — under whatever home the server listed it to them (EXO-90347).The third arm of the deployment's word, for what
isMintedByThisDeployment(long, java.lang.String)cannot see: a calendar a colleague imported rather than exported. Her pair is REMOTE, its slug is not her anchor, and its recorded path is under her home while the listing spells the same container under the sharee's — so neither the anchor nor the path answers, and the container uid, the last segment, is what both spellings share. Asked only once the server's own listing has named another owner for the collection, to tell a colleague's calendar (held here,CollectionOwnership.COLLEAGUES_EXO_CALENDAR) from one shared from outside this deployment (CollectionOwnership.SHARED); either way the collection is not adopted, so the answer names the more useful fact rather than deciding the adoption.- Parameters:
serverId- the declared server registrationcontainerUid- the collection's last path segment- Returns:
- true when a calendar binding of any user here records that container
-
containerUidOf
The container uid a collection is named by: its last path segment.- Parameters:
href- the collection path, canonical or not- Returns:
- the last segment, or null for a blank path
-
exportingUserOf
Which user of this deployment exported the calendar a collection stands for — the who form ofisMintedByThisDeployment(long, java.lang.String).Asked by the calendar list once
ownershipOf(long, java.lang.String, java.util.List<org.exoplatform.caldav.model.CalendarSync>, org.exoplatform.caldav.client.CalendarCollection)has classified a collection as a colleague's eXo calendar and the list wants to name the colleague (EXO-90237). The same two arms in the same order — the anchor the slug carries first, the recorded path second — so it names the user behind the very pair that made the boolean form say yes, and cannot name somebody else's. The boolean form stays the sweep's and the push's question, deliberately: they need only the yes, an existence query is cheaper than a row, and rewriting them over this one would add a fetch to every pass for an answer they discard.Null is a plain answer, not a failure: a path outside the outbound prefix, a collection another deployment minted, or a pair that vanished between the classification and this question all name nobody, and the caller says nothing about the owner rather than guessing.
- Parameters:
serverId- the declared server registrationhref- the collection path, canonical or not- Returns:
- the identity of the user whose EXO pair stands behind the collection, or null when no user of this deployment does
-
ownershipOf
public CollectionOwnership ownershipOf(long serverId, String principal, List<org.exoplatform.caldav.model.CalendarSync> usersPairs, CalendarCollection collection) Whose a listed collection is, to the user whose account listed it.The one classification the sweep, the calendar list and the event read-through act on (EXO-90234), combining the two witnesses
CollectionOwnershipnames. This deployment is heard first, and only about a collection under the outbound prefix: the slug carries the anchor eXo minted it with, and the question is whose calendar that anchor names here. The user's own pairs, already in hand, answer first and for free — an EXO pair of theirs carrying that anchor, or recorded at that path, makes itCollectionOwnership.OWN_EXO_CALENDAR, their own calendar met again under a path BlueMind republished it at. Failing that, the account-wide question (isMintedByThisDeployment(long, java.lang.String)) says whether another user of this deployment exported it, and a yes isCollectionOwnership.COLLEAGUES_EXO_CALENDAR: the colleague shared their eXo calendar, eXo already holds the original, and the user must never be given a writable copy of it. The server is heard second, for every collection, throughCalendarCollection.isSharedWith(String); a collection neither witness speaks against isCollectionOwnership.OWN.The deployment before the server, on purpose, although the server's word costs no query. On BlueMind the server is silent: a subscribed share is listed under the user's own home, names the user as owner and grants the full privilege set (observed live, 2026-09-13), so the deployment's word is the only one there is. On Stalwart both speak — the colleague's collection is listed at her path with her as owner and read-only — and hearing the deployment first names the more useful fact in the line the sweep writes: not merely "somebody else's", but "a calendar of this deployment, another user's". The cost is bounded as before: one account-wide question per pass per prefixed collection the user holds no pair for, which after this change is exactly the set of colleagues' shares and other deployments' collections.
The server's naming is heard between the two (EXO-90275). BlueMind lists a calendar the user subscribed to — a colleague's main or created calendar, a pool vehicle, a room — under the user's own home with the user as owner and the full privilege set, so neither server signal fires; the container uid it names the collection by carries the owner instead (
BlueMindContainerNaming). A uid other than the account's own isCollectionOwnership.SUBSCRIBED_PERSONorCollectionOwnership.SUBSCRIBED_RESOURCE. Before the server's two signals, so that a resource is told from a person wherever both could speak; after the deployment, because no eXo-minted slug has that shape. It costs no query. Until it was read, a subscribed resource and a subscribed colleague's main calendar were materialised as the user's own calendars (rig calendar 16, acceptance calendar 33).The server's own listing is heard last among the deployment's questions, and only for a prefixed collection the other three could not settle (EXO-90347). Everything above reads what eXo exported; what none of it can see is a calendar a colleague imported — made by eXo's naming elsewhere, adopted by her as a REMOTE pair whose slug is not her anchor — and then shared. On BlueMind that share is listed under the sharee's own home with the sharee as owner, so it classified as the sharee's own and was adopted (rig calendar 24). The one place BlueMind says whose it is, is its subscription listing (
AccountCalendarOwners): an owner other than the account's own makes it a share — a colleague's when a user here holds the container (isHeldByThisDeployment(long, java.lang.String)), somebody else's otherwise — and the account's own uid lets it through to adoption, however it was created: eXo is one CalDAV client among several, and a calendar the server says is the account's own is the account's to adopt. A listing that cannot be had, or that does not name the collection, withholds the answer rather than giving one: the naming and the DAV signals below are still heard, a collection they call a share is a share, and only one they would have called the account's own isCollectionOwnership.OWNER_UNKNOWN— nothing is adopted on it, and the next pass asks again. So the listing is heard after the deployment's two arms and before the server's own signals: where it withholds its answer it can only ever replace one that would have beenOWN, and where it names another owner it answers before the DAV signals — which on BlueMind cannot contradict it: the subscriber is the DAV owner there, so those signals call a writable share the account's own (the defect this arm closes) and a read-only one a share as well — and tells a colleague's share from an outsider's, which those signals cannot. A server that is not asked — not BlueMind — leaves this arm silent and the classification exactly as before.- Parameters:
serverId- the declared server registration, which scopes the account-wide questionprincipal- the account's owncurrent-user-principal, as the discovery walk answered it; null when the server named none, which leaves the owner comparison offusersPairs- every pair this user holds on this server, whatever its origin or statecollection- the listed collection- Returns:
- whose it is
-
ownershipOf
public CollectionOwnership ownershipOf(long serverId, String principal, List<org.exoplatform.caldav.model.CalendarSync> usersPairs, CalendarCollection collection, AccountCalendarOwners owners) Whose a listed collection is, with the server's own word on its owners heard — the form the sweep and the calendar list ask (EXO-90347).- Parameters:
serverId- the declared server registration, which scopes the account-wide questionprincipal- the account's owncurrent-user-principal, as the discovery walk answered it; null when the server named noneusersPairs- every pair this user holds on this server, whatever its origin or statecollection- the listed collectionowners- what the server says about who owns the account's calendars, asked once per pass; the silent witness for a server that is not asked, and read as silent when null- Returns:
- whose it is
- See Also:
-