Class CaldavSyncStorage
No business logic: what a pair means, when it may be created, whether a deletion propagates — all of that belongs to the service. What lives here is the mechanical part the service should not have to remember, above all that an href is stored decoded and without its trailing slash. A binding that compared raw hrefs would lose its pair the first time a server answered a differently-escaped one, and that is not a decision, it is an invariant.
-
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionstatic StringcalendarHomeOf(String href) The calendar home a collection sits under: its canonical href without the last segment.static StringcanonicalHref(String href) An href reduced to what identifies the resource: its percent-decoded path, without a trailing slash.longcountObjects(long calendarSyncId) How many objects a pair maps.voiddeleteObject(long id) Forgets one object mapping.intdeleteObjects(long calendarSyncId) Drops every object mapping of a pair, leaving the pair itself in place.voiddeletePair(long id) Removes a pair and, by the foreign key, its object mappings.org.springframework.data.domain.Page<Long> getDueAccounts(org.exoplatform.caldav.model.CalendarSyncStatus status, Date before, int offset, int limit) The users whose bindings are due, oldest waiting first.org.springframework.data.domain.Page<org.exoplatform.caldav.model.CalendarSync> getDuePairs(org.exoplatform.caldav.model.CalendarSyncStatus status, Date before, int offset, int limit) One page of pairs in a given state whose last synchronisation ended before a cutoff, or has never ended.org.exoplatform.caldav.model.CalendarSyncgetExoCalendarPairOnServer(long serverId, String anchor) The pair through which a calendar of this deployment, anyone's, is exported to one server under one anchor — the who form ofisExoCalendarOnServer(long, java.lang.String).org.exoplatform.caldav.model.CalendarSyncgetExoCollectionPairOnServer(long serverId, String href) The pair through which a calendar of this deployment, anyone's, is exported to one server at one collection path — the who form ofisExoCollectionOnServer(long, java.lang.String), for the shape where the slug is not the anchor and the recorded path is what recognised the collection.getMirrorEventId(long userIdentityId, long serverId, String icsUid) The eXo event a copy eXo wrote into this user's mirror stands for.getMirrorEventIdOnServer(long serverId, String icsUid) The eXo event a copy eXo wrote into any account of one server stands for (EXO-90247).org.exoplatform.caldav.model.ObjectSyncgetObjectByEvent(long calendarSyncId, long localEventId) The mapping for one eXo event inside a pair.org.exoplatform.caldav.model.ObjectSyncgetObjectByUid(long calendarSyncId, String icsUid) The mapping for one iCalendar object inside a pair.org.springframework.data.domain.Page<org.exoplatform.caldav.model.ObjectSync> getObjects(long calendarSyncId, int offset, int limit) One page of a pair's object mappings.org.springframework.data.domain.Page<org.exoplatform.caldav.model.ObjectSync> getObjectsByEvent(long localEventId, int offset, int limit) One page of the mappings of a single eXo event, across every user's collections.org.exoplatform.caldav.model.CalendarSyncgetPair(long id) One binding by its identifier, whoever it belongs to.org.exoplatform.caldav.model.CalendarSyncgetPairByLocalCalendar(long userIdentityId, long serverId, String localCalendarSyncUid) The pair bound to one local calendar.org.exoplatform.caldav.model.CalendarSyncgetPairByRemoteHref(long userIdentityId, long serverId, String remoteHref) The pair bound to one remote collection, matched on the canonical path.List<org.exoplatform.caldav.model.CalendarSync> getPairs(long userIdentityId, long serverId) Every pair a user holds on one server.List<org.exoplatform.caldav.model.CalendarSync> getPairsByOrigin(long userIdentityId, long serverId, org.exoplatform.caldav.model.SyncOrigin origin) The pairs of one origin a user holds on a server.static StringhomePrefixOf(String collectionHref) The LIKE pattern matching every collection of the account a collection belongs to, or null when the href names no account.booleanisCollectionHeldOnServer(long serverId, String containerUid) Whether a user of this deployment, anyone, holds a calendar binding on one server for a collection of one container uid, under whatever home the server listed it to them (EXO-90347).static booleanisDuplicateKey(RuntimeException failure) Whether a persistence failure is the database refusing a row for an integrity constraint — a duplicate key above all.booleanisEventMapped(long localEventId) Whether an eXo event is already mapped.booleanisExoCalendarOnServer(long serverId, String anchor) Whether a calendar of this deployment, anyone's, is exported to one server under one anchor.booleanisExoCollectionOnServer(long serverId, String href) Whether a calendar of this deployment, anyone's, is exported to one server at one collection path.booleanisMirrorOwned(long serverId, String collectionHref, String icsUid) Whether this iCalendar object is a copy eXo itself wrote into the user's mirror.booleanisMirrorOwnedByAnotherUser(long userIdentityId, long serverId, String collectionHref, String icsUid) Whether this iCalendar object is a copy eXo wrote into the mirror of a different user on this account.mappedEventIds(long userIdentityId, Collection<Long> localEventIds) Which of these eXo events already carry a live copy for this user.mappedEventIds(Collection<Long> localEventIds) Which of these eXo events carry a copy for ANYONE.org.exoplatform.caldav.model.ObjectSyncsaveObject(org.exoplatform.caldav.model.ObjectSync object) Creates or updates an object mapping, canonicalising its href on the way in.org.exoplatform.caldav.model.CalendarSyncsavePair(org.exoplatform.caldav.model.CalendarSync pair) Creates or updates a pair, canonicalising its href on the way in.
-
Constructor Details
-
CaldavSyncStorage
public CaldavSyncStorage()
-
-
Method Details
-
getPairs
Every pair a user holds on one server.- Parameters:
userIdentityId- identity of the userserverId- declared server registration- Returns:
- the user's pairs on that server
-
getPairByLocalCalendar
public org.exoplatform.caldav.model.CalendarSync getPairByLocalCalendar(long userIdentityId, long serverId, String localCalendarSyncUid) The pair bound to one local calendar.- Parameters:
userIdentityId- identity of the userserverId- declared server registrationlocalCalendarSyncUid- agenda's immutable calendar anchor- Returns:
- the pair, or null when the calendar is not bound
-
getPairByRemoteHref
public org.exoplatform.caldav.model.CalendarSync getPairByRemoteHref(long userIdentityId, long serverId, String remoteHref) The pair bound to one remote collection, matched on the canonical path.Resolved in memory over the user's own pairs rather than by a query: the href column is too long to index on MySQL under utf8mb4, and a person's pairs are few. Querying it would have meant either a scan or a schema contortion, for a set that fits in a handful of rows.
- Parameters:
userIdentityId- identity of the userserverId- declared server registrationremoteHref- the collection href, in any spelling- Returns:
- the pair, or null when the collection is not bound
-
getPairsByOrigin
public List<org.exoplatform.caldav.model.CalendarSync> getPairsByOrigin(long userIdentityId, long serverId, org.exoplatform.caldav.model.SyncOrigin origin) The pairs of one origin a user holds on a server. A list even forSyncOrigin.MIRROR, which should be single: the database cannot enforce that uniqueness, so the caller is given what is actually there rather than the first of several.- Parameters:
userIdentityId- identity of the userserverId- declared server registrationorigin- which side created the collection- Returns:
- the matching pairs
-
getDuePairs
public org.springframework.data.domain.Page<org.exoplatform.caldav.model.CalendarSync> getDuePairs(org.exoplatform.caldav.model.CalendarSyncStatus status, Date before, int offset, int limit) One page of pairs in a given state whose last synchronisation ended before a cutoff, or has never ended.- Parameters:
status- the state to selectbefore- pairs last synchronised strictly before this instantoffset- page offset, in pageslimit- page size- Returns:
- one page of due pairs, oldest synchronisation first
-
getDueAccounts
public org.springframework.data.domain.Page<Long> getDueAccounts(org.exoplatform.caldav.model.CalendarSyncStatus status, Date before, int offset, int limit) The users whose bindings are due, oldest waiting first. The account-wise form ofgetDuePairs(org.exoplatform.caldav.model.CalendarSyncStatus, java.util.Date, int, int), and what the sweep asks: batching bindings let one user's collections fill a whole run, so no other account was reached at all.- Parameters:
status- the binding state that counts as sweepablebefore- bindings last synchronised strictly before this instantoffset- page indexlimit- how many users one page carries- Returns:
- one page of user identities
-
getPair
public org.exoplatform.caldav.model.CalendarSync getPair(long id) One binding by its identifier, whoever it belongs to.Returns it without checking ownership on purpose: the check belongs to the caller that knows who is asking, and hiding it here would make a service look safe while the storage quietly decided for it.
- Parameters:
id- the binding's identifier- Returns:
- the binding, or null when there is none
-
savePair
@Transactional public org.exoplatform.caldav.model.CalendarSync savePair(org.exoplatform.caldav.model.CalendarSync pair) Creates or updates a pair, canonicalising its href on the way in.- Parameters:
pair- the pair to persist- Returns:
- the persisted pair, carrying its identifier
-
deletePair
@Transactional public void deletePair(long id) Removes a pair and, by the foreign key, its object mappings.- Parameters:
id- technical identifier of the pair
-
getObjectByUid
The mapping for one iCalendar object inside a pair.- Parameters:
calendarSyncId- the pairicsUid- the iCalendar UID- Returns:
- the mapping, or null when the object is unknown
-
isMirrorOwned
Whether this iCalendar object is a copy eXo itself wrote into the user's mirror.Ownership expressed per object rather than per collection. Until now a mirror copy was protected by where it lived — the inbound sweep skipped the dedicated collection wholesale — so pointing the mirror at a calendar the inbound half also reads would have removed the protection entirely, and eXo would have imported its own copies back as duplicate personal events.
The mapping table is the authority because its row is saved in the same flow as the PUT that created the object: by the time any inbound pass can see the object, the row exists. The one window it does not cover is an interruption between the two — the PUT went through and the row was never saved — which leaves an unowned copy the next push reconciles.
Asked for the account and not for one user (EXO-90190): a CalDAV account can be connected by several eXo users, and a copy one of them wrote into it is still eXo's. And for the account rather than the whole server registration, because a copy is a fact about the account it sits in — the same UID mirrored on another account of the server says nothing about this one, and calling it "ours" there drops a third user's genuine meeting. The account is named by the calendar home the collection sits under, derived from the collection's own href by
calendarHomeOf(String).- Parameters:
serverId- the declared server registrationcollectionHref- the href of the collection being read, in any spelling; the account is the calendar home it sits undericsUid- the iCalendar UID being imported- Returns:
- true when a mirror pair of any user on that account already maps that UID; false when the href names no home to ask about
-
isMirrorOwnedByAnotherUser
public boolean isMirrorOwnedByAnotherUser(long userIdentityId, long serverId, String collectionHref, String icsUid) Whether this iCalendar object is a copy eXo wrote into the mirror of a different user on this account.What the outbound half asks before writing a personal-calendar object (EXO-90190): two users' pairs on one collection compute the same href for one UID, so the write would land on the other user's copy and replace it. The user's own mirror is excluded because a UID it maps is that user's own copy, which a write of theirs may legitimately move or rewrite. Scoped to the account the collection sits in, like
isMirrorOwned(long, String, String): a mirror on another account of the server holds no copy this write could reach, and refusing for it kept a user from writing their own event into their own collection.- Parameters:
userIdentityId- identity of the user about to writeserverId- the declared server registrationcollectionHref- the href of the collection about to be written into, in any spellingicsUid- the iCalendar UID about to be written- Returns:
- true when another user's mirror pair on that account maps that UID; false when the href names no home to ask about
-
isExoCalendarOnServer
Whether a calendar of this deployment, anyone's, is exported to one server under one anchor.The account-scoped ownership question for calendar pairs (EXO-90226), the sibling of the per-object one
isMirrorOwned(long, java.lang.String, java.lang.String)asks for copies. An ORIGIN=EXO pair on the server for the anchor means the collection whose slug carries that anchor was minted by this deployment for a calendar that exists here — whichever user's, and whatever state the pair is in now. No such pair, and the collection was minted by another eXo deployment writing into the same account, which is a calendar this one has never seen.- Parameters:
serverId- the declared server registrationanchor- the calendar anchor the collection's slug carries- Returns:
- true when a user of this deployment holds an EXO pair for it; false when the anchor is blank, which names no calendar
-
isExoCollectionOnServer
Whether a calendar of this deployment, anyone's, is exported to one server at one collection path.The other arm of the same question (EXO-90226), by the recorded path rather than the anchor: for the collection a server republishes under a slug that is not the anchor eXo minted, which
isExoCalendarOnServer(long, java.lang.String)cannot recognise. Matched on the canonical path, which is how the href is stored, so the spelling the listing uses — host, percent-encoding, trailing slash — does not decide the answer. Every user and every status, as for the anchor.- Parameters:
serverId- the declared server registrationhref- the collection path, in any spelling- Returns:
- true when a user of this deployment holds an EXO pair recorded there; false when the href is blank, which names no collection
-
isCollectionHeldOnServer
Whether a user of this deployment, anyone, holds a calendar binding on one server for a collection of one container uid, under whatever home the server listed it to them (EXO-90347).The third arm of the ownership question, for the shape the other two cannot see: a colleague imported a calendar eXo's naming made elsewhere — a REMOTE pair, its slug not her anchor — and shared it, and BlueMind lists the one container under each subscriber's own home. The container uid, the collection's last segment, is what both spellings share. Every user, every status, every origin but the mirror ledger, which binds no calendar.
- Parameters:
serverId- the declared server registrationcontainerUid- the collection's last path segment- Returns:
- true when such a binding exists; false for a blank uid, which names no collection
-
getExoCalendarPairOnServer
public org.exoplatform.caldav.model.CalendarSync getExoCalendarPairOnServer(long serverId, String anchor) The pair through which a calendar of this deployment, anyone's, is exported to one server under one anchor — the who form ofisExoCalendarOnServer(long, java.lang.String).Asked once a listed collection is known to be a colleague's eXo calendar and the list wants to name the colleague (EXO-90237): the pair's user is the owner. Every user and every status, as for the boolean form. Should two users ever hold an EXO pair for one anchor on one server — the schema does not forbid it — the one named is the active pair, else the oldest; the query orders and the page of one picks, so the answer is the same on every listing.
- Parameters:
serverId- the declared server registrationanchor- the calendar anchor the collection's slug carries- Returns:
- the pair to believe, or null when no user of this deployment holds an EXO pair for the anchor, or the anchor is blank
-
getExoCollectionPairOnServer
public org.exoplatform.caldav.model.CalendarSync getExoCollectionPairOnServer(long serverId, String href) The pair through which a calendar of this deployment, anyone's, is exported to one server at one collection path — the who form ofisExoCollectionOnServer(long, java.lang.String), for the shape where the slug is not the anchor and the recorded path is what recognised the collection.Matched on the canonical path, as the href is stored; the same active-first, oldest-first pick as
getExoCalendarPairOnServer(long, String), for the same reason.- Parameters:
serverId- the declared server registrationhref- the collection path, in any spelling- Returns:
- the pair to believe, or null when no user of this deployment holds an EXO pair recorded there, or the href is blank
-
calendarHomeOf
The calendar home a collection sits under: its canonical href without the last segment.The account's identity, as far as the mapping table can tell it (EXO-90190). Nothing stores the home a pair was listed from, and nothing needs to: the listing that produces every remote pair is a depth-one PROPFIND of the home, and the collections eXo mints — the mirror, the outbound copies of the user's own calendars — are created directly under it. So the parent of a pair's href is its home on every server this connector has met, and two pairs share an account exactly when they share a parent. A server that nested calendars below the home would break that reading, in the safe direction: the question would find no mirror and answer "not ours".
- Parameters:
href- a collection href, in any spelling- Returns:
- the canonical home, or null when the href has no parent to name one — blank, a bare segment, or a child of the root
-
homePrefixOf
The LIKE pattern matching every collection of the account a collection belongs to, or null when the href names no account.- Parameters:
collectionHref- a collection href, in any spelling- Returns:
- the escaped pattern for
home/%, or null
-
isDuplicateKey
Whether a persistence failure is the database refusing a row for an integrity constraint — a duplicate key above all.Judged on the JDBC cause and not on the Spring type, because the type is not stable on this platform (EXO-90190). The Kernel hands Spring its own
EntityManagerFactoryas a plain bean — not anEntityManagerFactoryInfo— so theJpaTransactionManagerrunsDefaultJpaDialect, which turns every unrecognisedPersistenceExceptioninto aJpaSystemExceptionat commit; and noPersistenceExceptionTranslatorbean exists for the repository proxy to consult, so a failure raised inside the repository call — an insert executed at persist time, as identity columns do on MySQL — arrives as Hibernate's ownConstraintViolationException, aPersistenceExceptionand noDataAccessExceptionat all. Only a Boot-managed factory, the test slice's, yields theDataIntegrityViolationExceptiona reader would expect. What every shape shares is theSQLExceptionin its cause chain: the JDBC standard class23of its SQLState is an integrity violation on every driver, and MySQL's and HSQLDB's drivers also type it asSQLIntegrityConstraintViolationException; PostgreSQL's does not, which is why the SQLState is asked too. The deepestSQLExceptionis the one read: HSQLDB hangs its own internal exception below it.- Parameters:
failure- the exception a save surfaced- Returns:
- true when an integrity-constraint violation is in its cause chain
-
getMirrorEventId
The eXo event a copy eXo wrote into this user's mirror stands for.The companion of
isMirrorOwned(long, String, String)and asked in the same breath (EXO-89807): the inbound half recognises one of eXo's own copies and drops it, but the owner's answer is written on that copy and has to be recorded against something. The mapping that knows which event lives on the MIRROR pair, and the pair reading the collection is a different one, so nothing pair-scoped can answer this.Scoped to the user while its companion is not, and the difference is load-bearing (EXO-90190). An answer read off a copy is recorded as the reading user's. On an account two users share, the companion says "eXo's" of the other user's copy too — rightly, so it is not imported — but the event it names is the other user's to answer for. Asked here for the deployment, user six would record user one's phone answer as their own. On a foreign copy this answers null, nothing is recorded, and that is the behaviour: the copy's owner reads their own answer on their own pass.
Two mirror pairs holding the same UID is not a state this connector creates — a user has one mirror per server — and if it ever arises, the copies stand for different events and no answer can be attributed to either without guessing. Ambiguity therefore answers "no event", the same as "never pushed": nothing is recorded rather than something recorded against the wrong meeting.
- Parameters:
userIdentityId- identity of the user whose copies are asked aboutserverId- the declared server registrationicsUid- the iCalendar UID being read- Returns:
- the event the copy stands for, or null when it is not ours, names no event, or names more than one
-
getMirrorEventIdOnServer
The eXo event a copy eXo wrote into any account of one server stands for (EXO-90247).The question a receiving account has to ask when a server schedules for itself. BlueMind delivers the organizer's own copy into each invitee's account keeping its UID, so in the invitee's home the object sits on no pair of theirs and under no calendar home eXo wrote to: the account-scoped
isMirrorOwned(long, String, String)rightly answers "not ours here", andgetMirrorEventId(long, long, String)rightly answers "not this user's". Neither names the meeting, and the meeting is what the receiving pass needs so it can adopt the user's answer instead of importing a second event.This does not reopen EXO-90190. The defect there was an answer attributed to the wrong person. What this returns is only the identity of a meeting; whether the reading user may answer for it is asked of agenda separately, and which attendee line is read is decided by the reading user's own addresses. The user-scoped sibling stays exactly as it is and keeps feeding the adoption on the mirror path — the two are different questions and are deliberately two methods.
A UID is a random UUID minted per (series, user), so one matching across two accounts of a server is the same object travelling, not a collision. Ambiguity answers "no event", as the user-scoped sibling does and for the same reason: nothing recorded beats something recorded against the wrong meeting.
- Parameters:
serverId- the declared server registrationicsUid- the iCalendar UID being read- Returns:
- the event the copy stands for, or null when no mirror on this server wrote that UID, when it names no event, or when it names more than one
-
getObjectByEvent
public org.exoplatform.caldav.model.ObjectSync getObjectByEvent(long calendarSyncId, long localEventId) The mapping for one eXo event inside a pair.- Parameters:
calendarSyncId- the pairlocalEventId- the eXo event- Returns:
- the mapping, or null when the event has never been pushed
-
getObjects
public org.springframework.data.domain.Page<org.exoplatform.caldav.model.ObjectSync> getObjects(long calendarSyncId, int offset, int limit) One page of a pair's object mappings.- Parameters:
calendarSyncId- the pairoffset- page offset, in pageslimit- page size- Returns:
- one page of mappings, by identifier
-
getObjectsByEvent
public org.springframework.data.domain.Page<org.exoplatform.caldav.model.ObjectSync> getObjectsByEvent(long localEventId, int offset, int limit) One page of the mappings of a single eXo event, across every user's collections.The set an edit of that event is allowed to reach — every copy that already exists, and nothing else.
- Parameters:
localEventId- the eXo eventoffset- page offset, in pageslimit- page size- Returns:
- one page of mappings, by identifier
-
countObjects
public long countObjects(long calendarSyncId) How many objects a pair maps.- Parameters:
calendarSyncId- the pair- Returns:
- the mapping count
-
isEventMapped
public boolean isEventMapped(long localEventId) Whether an eXo event is already mapped. The question the backfill asks before creating a row, and the reason re-running it creates nothing.- Parameters:
localEventId- the eXo event- Returns:
- true when a mapping exists
-
mappedEventIds
Which of these eXo events already carry a live copy for this user. Asked for a whole list at once: the seeding pass asks about a user's whole upcoming window on every sweep, and asking one event at a time made the steady state — where every one of them is already copied — cost a query per meeting to learn there was nothing to do.Not the batch form of
isEventMapped(long), which counts any row: here a cleared row, the tombstone a removal leaves, is not a copy.- Parameters:
userIdentityId- the identity whose mappings count, so the copy made for the first attendee does not answer for every other attendeelocalEventIds- the eXo events to ask about- Returns:
- the identifiers among them this user holds a live copy of, empty when none are
-
mappedEventIds
Which of these eXo events carry a copy for ANYONE. Kept for the callers that genuinely ask the global question; a per-user pass wantsmappedEventIds(long, Collection)instead, or the first attendee copied answers for every other attendee.- Parameters:
localEventIds- the eXo events to ask about- Returns:
- the identifiers among them that are mapped
-
saveObject
@Transactional public org.exoplatform.caldav.model.ObjectSync saveObject(org.exoplatform.caldav.model.ObjectSync object) Creates or updates an object mapping, canonicalising its href on the way in.- Parameters:
object- the mapping to persist- Returns:
- the persisted mapping, carrying its identifier
-
deleteObjects
@Transactional public int deleteObjects(long calendarSyncId) Drops every object mapping of a pair, leaving the pair itself in place.- Parameters:
calendarSyncId- the pair- Returns:
- how many mappings were removed
-
deleteObject
public void deleteObject(long id) Forgets one object mapping.What a mapping with nothing behind it gets: dropping it is how the object becomes importable again, rather than being skipped for ever by a row that describes an event no longer there.
- Parameters:
id- the mapping's identifier
-
canonicalHref
An href reduced to what identifies the resource: its percent-decoded path, without a trailing slash.The Java counterpart of the browser connector's collectionPath. The same collection is written
%40by one server and@by a client, reported with and without a trailing slash, and reached through more than one host — none of which makes it a different collection. An href that cannot be parsed is returned trimmed rather than rejected: this is a normalisation, not a validation, and refusing to store an odd href would lose the binding rather than protect it.- Parameters:
href- a collection or object href, absolute or relative- Returns:
- the canonical path, or the trimmed input when it cannot be parsed
-