Class BlueMindImportWriter
- All Implemented Interfaces:
CalendarObjectWriter
Which version, and why that one. The import answers a task, not an
ETag, and the engine drives every later conditional write off the ETag it
recorded. There is exactly one channel that value may come from: the
Depth: 1 PROPFIND the verification pass lists the collection with
(CalDavClient#listResourceEtags), because that pass compares the
listing against the row and, once the content is confirmed equal, adopts
the listing's value into the row (CaldavMirrorVerificationService#adoptVersion:
"the version the server publishes means the one the collection listing
publishes"). A precondition read from any other channel — a
calendar-multiget REPORT, a GET, the PUT's own answer — would be
compared against a row the pass has meanwhile rewritten in the listing's
shape, and on BlueMind the shapes differ: the REPORT renders
SyncTokens.getEtag(path, version), quoted base64
(CalendarMultigetExecutor.java:123), while the listing renders a
child .ics through GetTag.fetch as the raw token
bmdav_<lnum>_0 (GetTag.java:46-56, "unsupported ressource
type", timestamp 0). Compared across channels, every update after the first
pass would be refused for ever. So this door reads the precondition from
the listing, records what the listing publishes after the write, and the
row, the pass and the emulation speak one shape.
What that buys, and what it does not. On BlueMind the listed value
of an object never moves, so the emulated If-Match refuses nothing
a client changed — which is exactly what BlueMind's own CalDAV PUT
does with a real If-Match: its DAV server reads only
If-None-Match (PutProtocol.java:65,
putQuery.setCreate(r.headers().contains("If-None-Match"))) and no
If-Match reader exists anywhere under
plugins/net.bluemind.dav.server. So the engine never relied on it
there: the verification pass's content comparison and answer adoption are
the guard on this server, through either door. What the listing does
answer reliably is existence: an object absent from it is absent,
which is what create-only needs. The check and the write are two calls, so
a client writing between them is not refused; that is the race the repair
path already accepts on every server.
Reading one object rather than the whole calendar. Read through the listing alone,
every conditional write cost one Depth: 1 ETag-only PROPFIND of
the whole collection before it and one after it; on BlueMind the
destination is the main calendar, so every push, answer, exclusion or
removal listed the user's entire calendar twice, and BlueMind logged one
INFO line per child each time (GetTag.java:53-54). This door now
asks two single-object questions instead, and asks the listing only when
their answers are not conclusive:
- Is the object there? — a one-href
calendar-multigetREPORT askinggetetagonly (CalDavClient#multigetEtags). BlueMind answers one response per item its store returned (CalendarMultigetExecutor.java:82,114-130) and none for a uid it does not hold, so a response for the href is presence — taken only when its href is byte-equal to the one eXo sent: the server spells it as<REPORT path> + uid + ".ics"(:117), the construction the listing hashes from (DavStore.java:402), so equality means theDepth: 0request path below hashes like the listing's child for this object, whatever the collection-wide verdict. Its version is the REPORT channel's, quoted base64 (:123), and is never recorded. No response is not taken as absence: a failed lookup answers the same empty document (:83-86), so absence is only ever the listing's word — which is whyputObject(org.exoplatform.caldav.client.CalDavEndpoint, java.lang.String, java.lang.String), whose accepting answer is absence, reads the listing directly and asks nothing else. Both multiget shapes were observed on the rig on 2026-09-16 (bluemind-report-multiget-one-href-getetag.captured.xml: one response,"Ym1kYXZfMzk4MDk2NjI5Nl8x";bluemind-report-multiget-missing-href.captured.xml: an empty multistatus). - Which version does the listing publish for it? — a
Depth: 0PROPFIND ofgetetagon the href (CalDavClient#readEtag). BlueMind resolves it through the sameds.from(path)(PropFindProtocol.java:69) and hashesdr.getPath()(GetTag.java:47,56,SyncTokens.java:42), so the token equals the listing's iff the request path equalscontainerPath + uid + ".ics"asaddEventsbuilds it (DavStore.java:402) — a hypothesis about spelling, observed to hold on the rig on 2026-09-16 for one object of the main calendar (bluemind-propfind-object-depth0-getetag.captured.xmlagainstbluemind-propfind-collection-depth1-getetag.captured.xml, the samebmdav_3980966296_0), and still verified live per collection below because one object on one server is an observation, not a proof. This read cannot answer existence either: the router assumes an.icsnode exists whenever its path is well formed (MethodRouter.java:162-175,DavStore.java:474-502, thedefault"assume yes"),ResType.VSTUFFmints the node from the path with no lookup, andDepth: 0adds only that node (DavStore.java:355-357); a missing object is answered 207 with a token — observed on the rig the same day on a probe href that did not exist (bluemind-propfind-missing-object-depth0.captured.xml,bmdav_3856992451_0). So it is asked only once presence is established.
Depth: 0 read answers on a collection, its token is compared with
the Depth: 1 listing's for the same href, the way the pass compares
(sameVersion(java.lang.String, java.lang.String)); the verdict is kept per collection for the life of
the process (depthZeroAgreesWithListing) and stated once at INFO.
Agreement makes later writes on that collection single-object reads only;
disagreement keeps them on the listing, at today's cost. And a refusal is
never pronounced on the single-object channel alone: when its version
differs from the one the caller conditions on, the listing is read and its
value compared instead (versionOf(org.exoplatform.caldav.client.CalDavEndpoint, java.lang.String, java.lang.String)); a listing that contradicts
the token there revokes the verdict as well — so even a verdict that went
stale can cost a listing, never a wrong 412 and never a recorded version
of the wrong shape. Every fall-back is taken at DEBUG. Request count per conditional write on the settled path: two
single-object requests before and two after, none listing the collection;
a create still lists once, before the import.
Switching a server onto this door. A row written over CalDAV holds the PUT's ETag, another shape again. Its first conditional write through this door is refused once, the engine settles it as a conflict, and the next verification pass — finding the listing's value differs from the row — fetches, compares against what eXo renders now, repairs the edit in and adopts the listing's value. One refusal, self-healing, and only on the transition.
What is taken out of the document. LAST-MODIFIED, and only
on this door. BlueMind's importer applies a series only when the file's
LAST-MODIFIED is null, later than the stored one, or its EXDATEs
changed (ICSImportTask.java:171-179; the file's value becomes
updated at VEventServiceHelper.java:381-382); eXo renders
it from the event's own modification date, which an answer does not move,
so a copy rewritten to carry an answer would be silently dropped. Without
the property the import always applies. BlueMind renders the stored
object's LAST-MODIFIED from its own item timestamp
(VEventServiceHelper.java:151) and IcsEquivalence ignores
that property on every server, so the verification pass sees no drift.
A bounded quirk to know about. When an import changes the start or
end of a series, BlueMind's merge empties the stored exceptions and
EXDATEs before re-adding the file's (EventChangesMerge.java:111-113
— from source, not yet observed on a server); a second import of the same
document restores them. The verification pass performs that second import
when it compares the copy and finds it short of what eXo renders, and it
compares only when the listing's value moved — which on BlueMind it does
not — or in a settings round. Until the rig says otherwise, treat a
date-changing edit of a series with exceptions as one to watch.
-
Constructor Summary
ConstructorsConstructorDescriptionBlueMindImportWriter(BlueMindCalendarImportClient importClient, CalDavClient calDavClient) The writer over the import API and the CalDAV client its reads use. -
Method Summary
Modifier and TypeMethodDescriptionintdeleteObject(CalDavEndpoint endpoint, String href, String ifMatch) Deletes one object, conditionally when a version is given.overwriteObject(CalDavEndpoint endpoint, String href, String icsData) Writes one object over whatever is there, with no precondition at all.putObject(CalDavEndpoint endpoint, String href, String icsData) Creates one object; an object already at the href is answered 412.updateObject(CalDavEndpoint endpoint, String href, String icsData, String ifMatch) Replaces one existing object, only when its version is still the one the caller last read.
-
Constructor Details
-
BlueMindImportWriter
@Autowired public BlueMindImportWriter(BlueMindCalendarImportClient importClient, CalDavClient calDavClient) The writer over the import API and the CalDAV client its reads use.- Parameters:
importClient- BlueMind's calendar REST APIcalDavClient- the CalDAV client, for the reads that learn whether an object is stored and under which listed version
-
-
Method Details
-
putObject
Creates one object; an object already at the href is answered 412.Create-only, emulated: an object the listing already carries at the href is answered 412 with its version, the way
If-None-Match: *would. The listing is read directly here: the answer that lets a create proceed is absence, and absence is the listing's word alone.- Specified by:
putObjectin interfaceCalendarObjectWriter- Parameters:
endpoint- the declared serverhref- the object's server-absolute path to create aticsData- the iCalendar text to store- Returns:
- the status and, when known, the stored version
-
overwriteObject
Writes one object over whatever is there, with no precondition at all.- Specified by:
overwriteObjectin interfaceCalendarObjectWriter- Parameters:
endpoint- the declared serverhref- the object's server-absolute pathicsData- the iCalendar text to store- Returns:
- the status and, when known, the stored version; never a 412
-
updateObject
Replaces one existing object, only when its version is still the one the caller last read.Replace-if-unchanged, emulated: an object the listing does not carry, or carries under another version than
ifMatch, is answered 412, the wayIf-Matchwould. Versions compare the way the verification pass compares them — quoting and a weak marker aside.- Specified by:
updateObjectin interfaceCalendarObjectWriter- Parameters:
endpoint- the declared serverhref- the object's server-absolute pathicsData- the iCalendar text to storeifMatch- the version the caller's own read answered, verbatim- Returns:
- the status and, when known, the stored version; 412 when the object moved on or is gone
-
deleteObject
Deletes one object, conditionally when a version is given.Conditional when a version is given, emulated the same way: gone is 404, another version is 412. The removal itself carries
sendNotifications=false.- Specified by:
deleteObjectin interfaceCalendarObjectWriter- Parameters:
endpoint- the declared serverhref- the object's server-absolute pathifMatch- the version to condition the removal on, or null to remove unconditionally- Returns:
- 200 or 204 removed, 404 or 410 already gone, 412 refused
-