Class CaldavPushService
This is the half of the migration that stops the browser from wielding the user's CalDAV credentials to build and PUT iCalendar objects. What changes for the user is nothing; what changes for us is that every write becomes assertable — a test can now claim "the mirror calendar exists after we said we created it", which was precisely the assertion nobody could write while the work happened in a page.
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic enumWhat became of one attempt to write an answer onto one copy. -
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final StringSomeone else changed the object since we last read it.static final StringThe name this add-on registers itself under as an agenda remote provider, in caldav-configuration.xml.static final StringThe server would not create a collection and no calendar could be adopted.static final StringThe stored credentials were rejected upstream.static final StringThe calendar server took the credentials and refused the write: a 403 on the PUT or DELETE, which is a server saying the account has no write privilege on that collection (EXO-90235).static final StringThe object about to be written is another eXo user's copy of a meeting: a mirror pair of a different user on the same server maps its UID, and the two pairs derive the same href from it (EXO-90190).static final StringThe registration asks for the account's own default calendar, and the account names none that its calendar home actually holds.static final StringThe collection eXo copies space events into, derived from this slug alone.static final StringHow the collection presents itself in the user's own calendar client.static final StringNo CalDAV account is connected, so there is nowhere to write.static final StringThe write failed for a reason the user cannot act on individually.static final StringThe server's registration declares a write channel no installed add-on serves —BLUEMIND_IMPORTwithout the add-on that speaks BlueMind's import API. -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionaddressesNaming(long userIdentityId) Every address a copy might name one person by, without needing them to have connected an account.addressesNaming(long userIdentityId, org.exoplatform.caldav.model.CaldavUserSetting settings) Every address a copy on this user's account might name them by.currentMirror(long userIdentityId, String username) The collection space events are copied into, creating it when it does not exist yet.voiddeleteEvent(long userIdentityId, String username, String icsUid) Removes one event's object from wherever this connector wrote it.ensureMirror(long userIdentityId, String username) Establishes the calendar the copies are written into, creating it when it is not there.voidexcludeOccurrence(long userIdentityId, String username, String icsUid, Instant occurrence) Removes one occurrence from a series without removing the series.static booleanisClassified(String code) Whether a code was deliberately placed in one category or the other.static booleanisKnownState(String code) Whether a push refusal describes a persistent state of the subject rather than a failure of the attempt.booleanisOrganizerOf(long eventId, long identityId) Whether a copy of this event names this user as its ORGANIZER, and so never as an ATTENDEE.mirrorDestination(long userIdentityId, String username) Where the copies of a user's space events are written, asked of the account now with exactly the resolutioncurrentMirror(long, java.lang.String)andensureMirror(long, java.lang.String)use, but withoutcurrentMirror(long, java.lang.String)'s leniency: a lookup that fails is thrown, never read as "no destination" (EXO-90253).org.exoplatform.caldav.model.ObjectSyncpushAgendaEvent(long userIdentityId, String username, long eventId) Copies one agenda event into the user's mirror calendar.booleanpushAnswer(long userIdentityId, String username, long eventId, String response) Carries an answer recorded in eXo out to the copy on the user's calendar server.pushAnswerOnto(long holderIdentityId, String username, org.exoplatform.caldav.model.ObjectSync copy, List<String> answererAddresses, String response, long eventId) Writes one person's answer onto somebody else's copy of the same meeting.renderAgendaEvent(long userIdentityId, long eventId, String icsUid) The calendar object eXo would write for one event right now, without writing it.org.exoplatform.caldav.model.ObjectSyncrewriteAgendaEvent(long userIdentityId, String username, long eventId) Writes the copy of an agenda event again, over whatever now stands in its place on the server.org.exoplatform.caldav.model.ObjectSyncwriteInto(long userIdentityId, String username, org.exoplatform.caldav.model.CalendarSync pair, org.exoplatform.caldav.model.IcsEvent event, Long localEventId) Writes one event into a collection this user is already bound to.
-
Field Details
-
MIRROR_COLLECTION_SLUG
The collection eXo copies space events into, derived from this slug alone.- See Also:
-
MIRROR_DISPLAY_NAME
How the collection presents itself in the user's own calendar client.- See Also:
-
NOT_CONNECTED
No CalDAV account is connected, so there is nowhere to write.- See Also:
-
CREDENTIALS
The stored credentials were rejected upstream.- See Also:
-
CONFLICT
Someone else changed the object since we last read it.- See Also:
-
SAVE
The write failed for a reason the user cannot act on individually.- See Also:
-
CREATION_REFUSED
The server would not create a collection and no calendar could be adopted.- See Also:
-
MAIN_CALENDAR_UNKNOWN
The registration asks for the account's own default calendar, and the account names none that its calendar home actually holds.Refused rather than worked around. Falling back to a calendar of eXo's own making would put the copies exactly where the administrator had just stopped asking for them, and picking one out of the listing is the guess this whole path exists not to make.
- See Also:
-
FOREIGN_COPY
The object about to be written is another eXo user's copy of a meeting: a mirror pair of a different user on the same server maps its UID, and the two pairs derive the same href from it (EXO-90190).A state, not a failure: the copy stays that user's for as long as the meeting does, and no retry changes whose it is. It arises on an account two users share, when one of them holds — from before the ownership question was widened — a personal event imported from the other's copy, and the push would carry it back onto the very object it came from.
Classified a known state so that it is logged without a trace, and given up on by the sweep the first time it is refused rather than after
exo.agenda.caldav.push.maxAttemptsrefusals — nothing was sent, and no waiting changes whose the copy is (CaldavEventPropagationService#refuse).- See Also:
-
FORBIDDEN
The calendar server took the credentials and refused the write: a 403 on the PUT or DELETE, which is a server saying the account has no write privilege on that collection (EXO-90235).A state, not a failure. Observed live on Stalwart: a calendar a colleague shared read-only had been materialised as the user's own, and the push of every edit they made into it was refused with 403 — then retried, five times over twenty-five minutes, for a refusal the server was never going to withdraw, while the log called it transient. The privilege is the server's to grant and only a person on that side changes it; retrying spends requests and says nothing new. So it is classified a known state, and the sweep gives up on the obligation the first time it is refused this way, naming the object the server refused (
CaldavEventPropagationService#refuse).The materialised calendar itself is left as it is: the sweep no longer makes one out of a share (
CaldavSyncService), and what to do with the ones made before it read ownership is a migration question, not a push's.- See Also:
-
WRITE_CHANNEL_UNAVAILABLE
The server's registration declares a write channel no installed add-on serves —BLUEMIND_IMPORTwithout the add-on that speaks BlueMind's import API. A state of the platform, not of the attempt: the copy is not written through CalDAV instead, because the channel exists to keep that server from receiving a CalDAV write, and only installing the add-on or switching the registration back to CalDAV clears it.- See Also:
-
CONNECTOR_NAME
The name this add-on registers itself under as an agenda remote provider, in caldav-configuration.xml. It has to match that declaration exactly: agenda resolves the provider by name when it stores the mapping between an eXo event and the object written for it.- See Also:
-
-
Constructor Details
-
CaldavPushService
public CaldavPushService()
-
-
Method Details
-
isKnownState
Whether a push refusal describes a persistent state of the subject rather than a failure of the attempt.The distinction exists for the log and nothing else: a known state is recorded at debug and without a trace, a failure at warn and with one. A user who has never connected an account is an ordinary state of that user, not an incident — printing eleven frames for it, once per attendee per meeting, buries the copies that genuinely failed under the ones that were never going to be made.
By itself it changes nothing about retries. A refused write stays owed either way, the sweep attempts it again either way, and after
exo.agenda.caldav.push.maxAttemptsrefusals — five, about twenty-five minutes at the sweep's cadence — it is abandoned either way and not read again until an edit of the meeting renews it. A known state is not "retried until the person acts": it is retried five times and then given up on, and the abandonment line says which kind it was. Two of the states are given up on at the first refusal instead —FOREIGN_COPYandFORBIDDEN— because for those a second attempt can only repeat the first; that is the sweep's decision, taken per code inCaldavEventPropagationService#refuse, not a property of being a known state.Anything unrecognised is a failure. Not silence: a code nobody classified is the one worth hearing about, and a default of "known state" would turn this from a filter into a silencer the next time the vocabulary grows.
- Parameters:
code- the code aCaldavPushExceptioncarries, may be null- Returns:
- true when only a person can clear it and retrying cannot
-
isClassified
Whether a code was deliberately placed in one category or the other.Exists for the test that walks the vocabulary and holds every code declared in this class to an explicit answer. Without it "failure" and "never classified" are the same answer from
isKnownState(java.lang.String), and a code added without a thought would look classified.- Parameters:
code- the code aCaldavPushExceptioncarries, may be null- Returns:
- true when the code is named in either category
-
writeInto
public org.exoplatform.caldav.model.ObjectSync writeInto(long userIdentityId, String username, org.exoplatform.caldav.model.CalendarSync pair, org.exoplatform.caldav.model.IcsEvent event, Long localEventId) Writes one event into a collection this user is already bound to.The same write for the space mirror and for a personal calendar: which collection an event belongs in is the caller's decision, and everything that follows — the conditional write, the merge, the mapping row — is the same regardless. Keeping one path means a defect fixed for one is fixed for both, which was not true while the browser held two of them.
- Parameters:
userIdentityId- identity of the user whose account is written tousername- their eXo login, which the credentials provider maps to their account on the serverpair- the collection to write intoevent- the event to copy, with identities already resolvedlocalEventId- the agenda event this object stands for, or null- Returns:
- the mapping row as it now stands
- Throws:
CaldavPushException- when the write cannot be carried out
-
pushAgendaEvent
public org.exoplatform.caldav.model.ObjectSync pushAgendaEvent(long userIdentityId, String username, long eventId) Copies one agenda event into the user's mirror calendar.The entry point the browser now calls instead of building iCalendar itself: it hands over an event id, and every decision that used to happen in the page — which identities are addressable, what the object looks like, where it goes, whether the write is conditional — happens here.
The event is read through agenda's own service, so its ACL applies: a user who may not see an event cannot have it copied into their calendar by asking for its id.
It takes no link back into eXo. It used to, and the browser used to build one and put it on the request — which meant only a browser push carried one, so the link appeared once and the next repair stripped it. The link is derived from the event by the mapper now, so every path renders the same one (EXO-89751).
A date poll is refused here and nothing is written for it — see
CaldavCopyPolicyfor why, and the private core below for why this is the only place that has to say so.- Parameters:
userIdentityId- identity of the user whose account is written tousername- their eXo login, which the credentials provider maps to their account on the servereventId- the agenda event to copy- Returns:
- the mapping row as it now stands, or null when nothing was written — the event is a date poll, or its calendar has no collection
- Throws:
CaldavPushException- when the event cannot be read or written
-
rewriteAgendaEvent
public org.exoplatform.caldav.model.ObjectSync rewriteAgendaEvent(long userIdentityId, String username, long eventId) Writes the copy of an agenda event again, over whatever now stands in its place on the server.The entry point for repairs, and the only one that writes unconditionally. A conditional write cannot repair anything: the condition is the stored etag, and an object needs repairing exactly when the server's etag has moved away from it — so the guarded path refuses every repair it is asked to make. What makes the overwrite legitimate is that the caller has already read the object, compared it against the eXo event, and decided which copy wins.
A repair is a write like any other, so it meets the same refusal: an event that has become a date poll is not repaired into place, it is left for the retirement that removes its copy.
- Parameters:
userIdentityId- identity of the user whose account is written tousername- their eXo login, which the credentials provider maps to their account on the servereventId- the agenda event whose copy is rebuilt- Returns:
- the mapping row as it now stands, or null when nothing was written — the event is a date poll, or its calendar has no collection
- Throws:
CaldavPushException- when the event cannot be read or written
-
renderAgendaEvent
The calendar object eXo would write for one event right now, without writing it.This is the baseline the mirror verification compares a server's copy against, and it is generated rather than stored. A recorded digest of what was pushed — or of what the server was seen to store just after — describes one moment and goes stale from the next: a server that finishes settling the object after the read-back, an upgrade that changes its serialisation, a deploy that changes ours. Regenerating costs one read of an event already in cache and can never be out of date.
The UID is taken from the mapping row rather than minted: this must have no effect on anything. Asking
adoptOrMintUidwould record a new identifier for an event that has one, on a path that is only supposed to be looking.It renders the link back into eXo, exactly as a push does. That was once the opposite: the link arrived on a push request, so this method had none to render, and the comparison had to exempt the
URLproperty or every copy would have been judged altered once and then stripped of the very link it was missing. Deriving the link from the event (EXO-89751) is what makes this baseline the same document a push writes, property for property, and what let the exemption go.- Parameters:
userIdentityId- identity of the user whose copy it iseventId- the agenda event to rendericsUid- the iCalendar identifier the copy is written under- Returns:
- the object as text, or null when the event can no longer be read — it may have been deleted, or hidden from this user, and neither is something to conclude a rewrite from
-
deleteEvent
Removes one event's object from wherever this connector wrote it.Every collection the user has, not only the mirror. An event of one of their own calendars is written into that calendar's collection, so a removal that looked only in the mirror would find nothing and quietly succeed — leaving the object on the server for ever, in the one place the user is most likely to notice it. A copy is written in one place and has to be removable from that same place.
A deletion whose object is already gone is a success, not a failure: the end state the caller asked for is the end state that holds.
- Parameters:
userIdentityId- identity of the userusername- their eXo login, which the credentials provider maps to their account on the servericsUid- the iCalendar UID of the event to remove- Throws:
CaldavPushException- when the deletion cannot be carried out
-
excludeOccurrence
public void excludeOccurrence(long userIdentityId, String username, String icsUid, Instant occurrence) Removes one occurrence from a series without removing the series.RFC 4791 puts every component of a series in one object, so a deletion here is a rewrite: the override carrying that instance is dropped and the master gains an EXDATE for it. Deleting the object instead would cancel every meeting of the series to cancel one.
The rewrite is conditional on the ETag last seen, so a series someone else changed in the meantime surfaces as a conflict rather than being overwritten with a stale copy.
- Parameters:
userIdentityId- identity of the userusername- their eXo login, which the credentials provider maps to their account on the servericsUid- the iCalendar UID of the seriesoccurrence- the instance to exclude- Throws:
CaldavPushException- when the rewrite cannot be carried out
-
pushAnswer
Carries an answer recorded in eXo out to the copy on the user's calendar server.The outward half of answering a meeting, and the one that was missing. A user can answer in two places — their calendar client, or the Accept and Decline links in the notification mail — and only the first of the two ever reached both sides. An answer given in eXo stopped at eXo's database: the copy went on displaying the previous answer to the person who had just changed it, and, worse, went on being the thing the verification pass reads a client's answer back from. The next time any client rewrote that object for any reason — a rename, a drag, a re-serialising sync — the stale answer on it was adopted as if it were fresh and the newer one was silently lost.
A targeted rewrite of one ATTENDEE's PARTSTAT, not a re-push of the whole event, and conditional on the ETag last seen. Both follow from what this knows: it has been told an answer, it has not read the object, so it is in no position to decide anything about the rest of it. A concurrent edit is therefore refused rather than overwritten — which is the same discipline every other ordinary write here follows, and leaves the divergence to the verification pass, whose job is precisely to look before it writes.
Answers false for every ordinary reason there is nothing to do — no connected account, no copy of this meeting, a copy that already says this. Every one of those says so in the log before it returns. This method did all of it silently once, and a propagation that silently does nothing is indistinguishable from the defect it was written to fix: it cost a whole live test round to find out which guard had refused. The ordinary no-ops are DEBUG; the states that should not arise — a mapping with no ETag, an object the server serves as empty, a copy that does not name the user it belongs to — are WARN, because each is an inconsistency somebody has to repair rather than a user simply not using the feature.
An organiser answering their own event is an ordinary no-op, and used not to read as one (EXO-89895). Their own copy names them on its ORGANIZER line and never on an ATTENDEE one, so the last of those WARNs — "the copy at … names none of …" — fired for a situation that is structurally impossible to avoid and entirely normal, for every organiser, for ever. Nothing broke: no obligation is recorded here and nothing retries. But an inconsistency nobody can repair is not an inconsistency, and a warning nobody can act on is how people learn to ignore warnings. It is now recognised by
isOrganizerOf(long, long)and declined at DEBUG, the same question the fan-out asks and from the same place. The WARN stays for the genuine case: a copy naming none of a non-organiser's addresses really is a state somebody has to repair.- Parameters:
userIdentityId- identity of the user whose answer was recordedusername- their eXo login, which the credentials provider maps to their account on the servereventId- the agenda event answered — an occurrence is resolved to its series, which is what the copy is written underresponse- the answer as agenda holds it, e.g.ACCEPTED- Returns:
- true when the copy was rewritten
- Throws:
CaldavPushException- when the copy could not be written
-
addressesNaming
public List<String> addressesNaming(long userIdentityId, org.exoplatform.caldav.model.CaldavUserSetting settings) Every address a copy on this user's account might name them by.There is more than one, and assuming otherwise is what made this propagation silently do nothing on a live rig. A copy names the account's own owner by the address their CalDAV account answers to — that is what lets a calendar client recognise the meeting as an invitation to itself and offer to answer it — while every other line on that object, and every object written before that rule existed, carries the person's eXo profile address. On the rig the two were
alice@…andbob@…for the same user, so asking the profile alone matched no line at all.Both are offered and the object decides. Neither is authoritative here on purpose: an address that names nobody costs one failed comparison, while the wrong single answer costs the whole feature, without a sound.
The account half is optional, and that is what EXO-89868 needed of it. Fanning an answer out to the other attendees' copies has to name the answerer on somebody else's object, and the answerer need not have connected an account at all — their answer still has to reach the copies of the people who did. A null or half-filled account is therefore an ordinary input here and contributes nothing rather than throwing; the profile address alone is exactly what a copy written for somebody else names them by, because
AgendaEventIcsMapper.attendeesspells the account address on one line only, the line of the person the copy is being written for.- Parameters:
userIdentityId- identity of the user whose answer was recordedsettings- their connected account, null or unconnected when they have none — then only their profile address is offered- Returns:
- the addresses to look for, most specific first, possibly empty
-
addressesNaming
Every address a copy might name one person by, without needing them to have connected an account.The form EXO-89868's fan-out asks for. The two-argument sibling is called from paths that are already holding the account they are writing to, and making them look it up again would be a second read of the same row; this one is called about a person whose account is not the one being written to and may not exist at all, so it looks the account up itself and treats its absence as ordinary rather than as a refusal.
The account is read but not required to be connected. A stored account with no password is not something this can write with — and it is not being asked to write with it. It is being asked what a copy might spell this person as, and a username stored without a password is still a username some copy may have been written under.
- Parameters:
userIdentityId- identity of the person to be named- Returns:
- the addresses to look for, most specific first, possibly empty
-
isOrganizerOf
public boolean isOrganizerOf(long eventId, long identityId) Whether a copy of this event names this user as its ORGANIZER, and so never as an ATTENDEE.Why answering has to ask, on both of its halves
Because a copy never carries an organiser's participation status, so there is nothing on one for an answer to rewrite — structurally, on every holder's copy including their own, by design.
IcsWriter'sguestsexcludes whoever heads the ORGANIZER line from the ATTENDEE lines (EXO-89768: a server whose model holds an organizer and a list of attendees that excludes them silently drops the duplicate, and the repair loop that followed could never close). In eXo they are an attendee and their answer is recorded like anybody's; the copy simply never says so.Measured, not reasoned, and twice. EXO-89868 found it on the fan-out: on the rig, root organised event 1040 and answered it, and every other holder's copy refused the write with "it names none of [root's address], so there is no participation status of theirs to rewrite". EXO-89895 found the same refusal on
pushAnswer(long, java.lang.String, long, java.lang.String), which writes the answerer's own copy and had never been told: "Answer of user 1 to event 1041 is not carried out: the copy at /dav/calendars/__uids__/751E….ics names none of [anais.francois@demo3.livecollab.fr]".Why it lives here, once, rather than in each caller
Because two callers now need it and a rule that recognises one side of a pair and not its neighbour is the failure shape this delivery has paid for repeatedly.
CaldavEventPropagationService.propagateAnswer(long, long, java.lang.String)asked it privately from EXO-89868;pushAnswer(long, java.lang.String, long, java.lang.String)needed the same answer from EXO-89895, and the fan-out already reaches across for the sibling question —addressesNaming(long), "by what addresses would a copy name this person" — so this is where the questions about whom a copy names, and how already live. It is also the class holdingAgendaEventIcsMapper, whoseorganizerOfandorganizerIsPusherare the render this predicate mirrors, andAgendaEventService, the read it needs: no new collaborator, and no second reading of the event that could drift from this one. The arrow only runs this way — the fan-out depends on this service and not the reverse — so asking it of the fan-out instead would have been a bean cycle.CaldavCopyPolicywas the other candidate and is ruled out by its own contract: it takes an event it does not read and deliberately refuses to answer for one that cannot be read, which is exactly the half this has to own.What this deliberately does not fix
Whether a copy should name an attending organiser on both lines. RFC 5545 expects both, and Google, Outlook and macOS all emit both, so there is a real case that the render is what is wrong here. That is a product decision Benjamin is keeping apart from this change, and it belongs to neither caller: if it is ever taken, it is
IcsWriter.gueststhat changes, and this method is what comes out — the skip exists only for as long as the render omits the line.Decided the way the mapper decides it
On
getCreatorId(), by identity, which is whatAgendaEventIcsMapper.organizerOfandtoIcsEvent'sorganizerIsPusherboth use. Emphatically not by comparing addresses, althoughguestscompares addresses: the creator's own attendee line is spelled with the address their CalDAV account answers to and their ORGANIZER line with their eXo profile address, so an address comparison answers differently for the same person depending on whose copy is being looked at — and on their own copy, which is whatpushAnswer(long, java.lang.String, long, java.lang.String)writes, it is the two spellings of one person that would be compared. A skip keyed on anything but the render's own predicate would drift from the render it mirrors.Read on the event as given rather than on its series, for the same reason:
toIcsEventis handed the occurrence when an occurrence is pushed and readsgetCreatorId()off that, so an override whose creator differs from its series' must be judged by its own. Note thatpushAnswer(long, java.lang.String, long, java.lang.String)does resolve an occurrence to its series — but only to find the object the copy is filed under, which is a different question with a different answer, and it is asked separately byicsUidOf.Read without a viewer, like the fan-out's own
readEventdoes. This is asked about the shape of the render and not about anybody's visibility, and a viewer-scoped read would make the answer depend on who happened to be asking — the very thing the identity comparison is here to avoid.An event that cannot be read answers false, and the caller goes ahead. Proceeding costs at worst the doomed write this method exists to prevent, for as long as agenda cannot answer, while refusing on an unreadable event would silently drop the answers of people who are not organisers at all — which is the defect this whole delivery is about.
- Parameters:
eventId- the agenda event answered, master or occurrenceidentityId- identity of the user whose answer was recorded- Returns:
- true when a copy of this event names them as its organizer
-
pushAnswerOnto
public CaldavPushService.AnswerOutcome pushAnswerOnto(long holderIdentityId, String username, org.exoplatform.caldav.model.ObjectSync copy, List<String> answererAddresses, String response, long eventId) Writes one person's answer onto somebody else's copy of the same meeting.The half of answering that was never built (EXO-89868)
pushAnswer(long, java.lang.String, long, java.lang.String)opens by reading the answerer's own account and rewrites the copy sitting on it. So an answer travelled to the answerer's own calendar and stopped there: every other attendee's copy went on showing the answer it last carried, on every server, for ever. The organiser read a stale RSVP in the client they actually live in, while eXo's own screens showed the right one — the worst shape a divergence can take, because nothing in the product disagrees out loud.This is the same targeted write pointed at a different copy. Two things are deliberately crossed over, and getting either the wrong way round is the whole defect written a second time: the holder's account, endpoint and ETag decide where and how the write goes, while the answerer's addresses decide which ATTENDEE line on that object is rewritten.
Why a targeted line rewrite and not a rewrite of the event
Because a full rewrite would destroy answers.
IcsMerger.merge(java.lang.String, java.lang.String, boolean)replaces the master VEVENT wholesale with what eXo renders, and on a server that records its own owner's answer without moving the ETag — BlueMind, measured on this rig — the conditional write succeeds and takes an unread answer down with it. The verification pass guards that case by reading before it writes; the push path does not read. Rewriting one named ATTENDEE line and leaving every other byte alone avoids it by construction, and that property is what the fan-out is safe on rather than something it is careful about.The mapping is handed in rather than looked up, and for a reason worth more than the round trip it saves: the caller has already recorded an obligation against that row, and a lookup here could resolve a different one — the same user can hold a mapping in their mirror and another in a personal collection. The copy written and the obligation settled must be the same copy.
- Parameters:
holderIdentityId- identity of the user whose copy is written tousername- their eXo login, which the credentials provider maps to their account on the servercopy- the mapping row naming that copy, with its href and the ETag the write is conditioned onanswererAddresses- every address the copy might name the answerer by, fromaddressesNaming(long)response- the answer as agenda holds it, e.g.ACCEPTEDeventId- the agenda event answered, master or occurrence: an answer to one occurrence moves that instance of the copy only- Returns:
- what happened to the copy, which is what tells the caller whether anything is still owed to it
- Throws:
CaldavPushException- when the copy could not be written
-
currentMirror
The collection space events are copied into, creating it when it does not exist yet.Ported from the browser connector, including the parts that look redundant and are not. The path is derived from the slug alone, with nothing random in it, so asking twice for the same calendar means asking for the same collection — a random suffix made every request a different one, and a user who disconnected and reconnected collected a new calendar on the server each time.
This reads it; it never creates one. Nothing configured is an answer, not a reason to make a calendar on someone's account — the settings screen asks this question on every render.
The name comes from the server on each call rather than from anything stored: the user may have renamed the calendar in their own client, and the screen showing a stale name is how a destination stops being recognisable as the one it names. Recorded href or not, the account is asked. Disconnecting clears the href while leaving the collection on the server, so a reconnected account has a destination it does not remember — and answering "none" there made eXo offer to create a calendar that already existed, and invite a second one beside it. The collection eXo creates lives at a path eXo derives, so it can be recognised without having been remembered: the same second candidate
ensureMirror(long, java.lang.String)adopts. The two now answer alike, where before creation adopted what this reported absent. A destination that was recorded and cannot be reached still raises: the user chose it, so the screen must say the account is unreachable rather than quietly report having no destination. With nothing recorded there is no such claim to keep, and an unreachable server answers null. Which of those candidates is asked for depends on the registration's destination setting, and it is asked through the same helperensureMirror(long, java.lang.String)uses: the screen must never name a calendar the next push would not write to.- Parameters:
userIdentityId- identity of the userusername- their eXo login, which the credentials provider maps to their account on the server- Returns:
- the destination and its current name, or null when the account has none — neither the one recorded nor one at the derived path
-
mirrorDestination
Where the copies of a user's space events are written, asked of the account now with exactly the resolutioncurrentMirror(long, java.lang.String)andensureMirror(long, java.lang.String)use, but withoutcurrentMirror(long, java.lang.String)'s leniency: a lookup that fails is thrown, never read as "no destination" (EXO-90253). For a caller that must not mistake an unreachable server for an account without copies — the share drawer, which warns when the calendar being shared holds them. Nothing is written.- Parameters:
userIdentityId- identity of the userusername- their eXo login, which the credentials provider maps to their account on the server- Returns:
- the destination, or null when the account really holds none
- Throws:
CaldavPushException- when the user has no connected accountRuntimeException- when the account cannot be asked
-
ensureMirror
Establishes the calendar the copies are written into, creating it when it is not there.The one place the destination is decided. Every push arrives here, and the registration's
mirrorTargetis read here and nowhere else: not in the mapper, which describes an event and does not place it; not in the sweep, which decides when to write and not where; not in the listeners, which decide whether. A second reader would be a second answer, and the two would disagree on exactly the account where it mattered.What "establishes" means depends on the kind, and only one of the two creates anything: the dedicated calendar is created when absent and an existing one adopted when the server refuses; the account's own default is asked for and never invented.
- Parameters:
userIdentityId- identity of the userusername- their eXo login, which the credentials provider maps to their account on the server- Returns:
- where the copies go, and whether an existing calendar was adopted
- Throws:
CaldavPushException- when no destination can be established —MAIN_CALENDAR_UNKNOWNwhen the account names no default calendar,CREATION_REFUSEDwhen nothing could be created or adopted
-