Class IcsEquivalence
Why meaning and not bytes. The mirror pass used to compare a digest of the object against a digest recorded at push time. That rests on a CalDAV server being a blob store, and it is not: a calendar object is a structured document, and a server that parses it into its own model and re-serialises it is behaving normally — RFC 4791 asks a server to preserve the object's semantics, not its bytes. Against BlueMind no byte baseline could be captured at all: recording what was sent made every copy look tampered with, and reading the object back after the write captured a state the server was still settling and then finished settling without moving the ETag. Both are attempts to learn one server's serialisation. This class understands the content instead, so there is nothing left to record: the baseline is regenerated from the eXo event on every comparison, and no digest is stored anywhere.
What is compared, and why exactly that. Only the single VEVENT eXo's
own render describes, matched by UID and RECURRENCE-ID, with its VALARMs. That
is not a convenience: it is the scope a repair can act on. A repair
writes through IcsMerger, which replaces that one component and
deliberately leaves the rest of the document exactly as it found it — the
enclosing VCALENDAR's own properties, its VTIMEZONEs, and any other VEVENT
another client authored. Reporting a difference outside that scope would
produce a repair that changes nothing, and a pass that reports the same
difference again five minutes later, for ever. Every property compared here is
one a repair actually rewrites.
Conservative by construction. The failure that matters is declaring
"equal" over a change a user made, which loses their edit silently. So within
the compared component the recognised set is closed — it is exactly what
IcsWriter emits — and anything outside it counts as a
difference: an unknown property, a property eXo does not emit, a
parameter that is not a documented default. The relaxations are enumerated in
IGNORED_EVENT_PROPERTIES, IGNORED_PARAMETERS and
DEFAULT_STATEMENTS, each with the reason it is safe, and each is a
statement about the record rather than about the meeting.
A document's own structural properties are ignored wherever they sit.
VERSION, PRODID and CALSCALE describe the document,
not the meeting, and this comparison never looked at them at calendar level.
BlueMind writes VERSION:2.0 inside the VEVENT, which is not
conformant and is still not a disagreement — so they are ignored by name
rather than by position. See STRUCTURAL_PROPERTIES.
One exemption inside the attendee set, and it is narrow on purpose. A
server may attach the calendar's own owner to an event that lands in their
calendar — BlueMind writes
ATTENDEE;CN=FRANCOIS;DIR=bm://19d43... on every copy — and that is
permitted behaviour of the same kind as re-ordering properties. So an
attendee line naming the owner, present on the server's copy and not on
eXo's render, and stating no answer, is not a difference. Everything
else about the roster stays strict: any other attendee added or removed is a
difference, an owner line eXo states and the copy has lost is a difference,
and every PARTSTAT — the owner's included — is compared as before. See
tolerated(java.lang.String, int, int).
Names are the server's to spell. CN and DIR are
dropped from every compared statement: a server substituting its directory's
own name and pointer for a person says nothing about the meeting, and the
address — which is who they actually are — is compared regardless.
And so is the server's own index of them. EMAIL restates the
address the property's value already carries, for a server that wants one
without parsing a URI; macOS Calendar writes it on the organizer and attendee
lines and IcsWriter never writes it at all. Compared, it made one
organizer two statements — the copy's and eXo's, each missing from the other
side — and every copy a macOS client had touched was repaired for it
(EXO-89826).
And an alarm the client stamps its own identifier on. An alarm is
compared as one folded statement of the event that carries it, so a single
property inside it that one side does not state makes the whole reminder a
different reminder. macOS Calendar keeps the reminder eXo pushes and writes
X-WR-ALARMUID on it — Apple's spelling of the RFC 9074 UID
that was already ignored — and the copy was read as carrying one alarm eXo
had never written while missing the one eXo had. See
IGNORED_ALARM_PROPERTIES (EXO-89828).
And an attendee the server did not keep. BlueMind discards attendees whose addresses are not in its directory, so a copy carries fewer people than eXo sent and no repair can change that. Tolerated for the same reason and in the opposite direction — which is why the two rules are stated separately and each names its own side. The cost is recorded where it belongs: the copy understates the guest list, and an attendee the server dropped cannot answer from it.
One guest is one statement, however each side spells them. A server
that keeps an attendee but not their answer states that person on a line eXo
does not carry, while eXo's own line for them is carried on no copy — two
divergences, one each way, about one person. Read as two they are read
wrongly, and the tolerance above is what does it: it absorbs eXo's half and
leaves the copy's half to be reported as an attendee the server added,
which is a different event in the world and the wrong thing to tell an
administrator (EXO-89829). So a person both copies name is folded into one
statement about them before any tolerance rule sees either half —
exactly one decision is taken for the pair, the rules above only ever meet
somebody one side does not name at all, and the disagreement is reported as
the one thing it is. What stops eXo re-asserting an answer a server keeps
stripping is not silence here but the pass's own budget: one copy is
re-pushed maxRepairs times and then abandoned, with a warning. See
pairAttendeeStatements(java.util.Map<java.lang.String, java.lang.Integer>, java.util.Map<java.lang.String, java.lang.Integer>, java.util.Map<java.lang.String, org.exoplatform.caldav.ics.IcsEquivalence.AttendeeLine>, java.util.List<org.exoplatform.caldav.model.IcsDivergence>, boolean).
Free text is compared for its words, not its layout. A TEXT value is
the one place where the document's line discipline and the content are
spelled in the same characters, and a server re-serialising a multi-line
description has to fold it. BlueMind returns eXo's blank line between two
paragraphs as a newline and a continuation space, which left every copy on a
live account permanently altered. Runs of whitespace are collapsed in
TEXT_PROPERTIES before comparison; every other character still
counts, so the tokenised answer links the description carries since
EXO-89753 stay guarded.
And a link a server wrote out twice. BlueMind auto-linkifies: it
appends every URI in a description a second time, in angle brackets,
immediately after the one already there — the conference link, the event link
and all three answer links at once. A bracketed URI is dropped only when it
exactly repeats the URI immediately before it; anything else about the
text is compared as before. It is a backreference, not a rule about angle
brackets and not a relaxation of URI equality, so a link that differs by one
character — a rewritten answer token above all — is still an edit. See
LINKIFIED_URI_REPEAT, and LINKIFIED_PROPERTIES for why only
the description is covered.
And a property a server stored twice. The same server keeps two
identical URL lines where eXo wrote one. That is not a value
difference but a cardinality one, so it gets its own rule: a statement eXo
also makes, surplus on the server's side, on a property
IcsWriter emits, is the server repeating itself. Every distinct
statement is still compared exactly — a copy holding eXo's link twice and a
link of its own still reports the second one. See
isServerSideRepetition(java.lang.String, int, int).
Two excusals, pointing opposite ways, declared per server. One excuses
an unrecognised property a server adds; the other a property eXo emits
that a server declines to store — BlueMind keeps no CONFERENCE. Both
are empty by default and neither can make two differing values compare equal,
bar the one catalogue entry that says so. See ServerExcusals; the
fields below are only the deployment-wide fallback.
What the parser settles before this sees it. ical4j unfolds continued
lines, unescapes TEXT values, trims a value's surrounding whitespace and
canonicalises a duration, so none of those can register as a difference and
none of them is redone here. That is a dependency, not an accident:
IcsEquivalenceTest characterises all four, so an upgrade that changed
any of them would be caught here rather than on somebody's calendar.
Three answers, not two. DIFFERENT and EQUIVALENT are
the judgements; UNJUDGEABLE is what comes back when eXo's own render
cannot be read as a calendar object holding one event. That is a defect on
this side, and the caller leaves the copy alone rather than overwriting a
user's calendar on the strength of it.
-
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptioncompare(String serverIcs, String exoIcs, Collection<String> ownerAddresses) Compares the object a server holds against the object eXo would write for the same event now.compare(String serverIcs, String exoIcs, Collection<String> ownerAddresses, String serverIgnoredProperties, String serverDroppedProperties) Compares the object a server holds against the object eXo would write for the same event now, excusing what this server has been declared to do.The fallback for a registration that has never been asked, dropping side.The fallback for a registration that has never been asked, adding side.
-
Constructor Details
-
IcsEquivalence
public IcsEquivalence()
-
-
Method Details
-
getGlobalIgnoredProperties
The fallback for a registration that has never been asked, adding side.- Returns:
- the configured value, never null
-
getGlobalDroppedProperties
The fallback for a registration that has never been asked, dropping side.- Returns:
- the configured value, never null
-
compare
Compares the object a server holds against the object eXo would write for the same event now.- Parameters:
serverIcs- the calendar object as the server holds itexoIcs- the calendar object eXo's engine renders for the event nowownerAddresses- every address a copy on this account may name its own owner by — the address the CalDAV account answers to and the eXo profile address, which differ in practice. Both are accepted, for the reasonOWNER_ATTENDEErecords. May be null or empty, which simply means no attendee is treated as the owner.- Returns:
- the judgement, carrying what diverged when it is
IcsJudgement.Verdict.DIFFERENT
-
compare
public IcsJudgement compare(String serverIcs, String exoIcs, Collection<String> ownerAddresses, String serverIgnoredProperties, String serverDroppedProperties) Compares the object a server holds against the object eXo would write for the same event now, excusing what this server has been declared to do.The two lists arrive as arguments rather than being read once at boot, which is the whole point of EXO-89771: an administrator ticking a box in the drawer changes what the next sweep concludes, with no restart.
ServerExcusalsholds what each of them excuses, and why null and an empty string are different answers.- Parameters:
serverIcs- the calendar object as the server holds itexoIcs- the calendar object eXo's engine renders for the event nowownerAddresses- every address a copy on this account may name its own owner by; may be null or emptyserverIgnoredProperties- patterns this server is excused for adding, or null to fall back to the deployment-wide propertyserverDroppedProperties- patterns this server is excused for not keeping, or null to fall back to the deployment-wide property- Returns:
- the judgement, carrying what diverged when it is
IcsJudgement.Verdict.DIFFERENT
-