Interface CalDavClient

All Known Implementing Classes:
HttpCalDavClient

public interface CalDavClient
The CalDAV protocol, and nothing else: it fetches, it does not decide. No business rule, no storage, no notion of what an event means locally — the sync engine owns all of that. It exists as an interface so the engine can be tested against canned answers, and so a different transport could be dropped in without the engine noticing — the exact seam email-connector's CardDavClient cut for CardDAV.

Two disciplines hold on every method:

  • Targets come from the registry, never from a caller. Every method addresses a CalDavEndpoint, which only endpoint(Long, String) can mint, and only from the administrator-managed server registry; hrefs are server-absolute paths resolved against that endpoint's authority, and an absolute href naming any other host is refused, not followed.
  • A 207 is never read as success by itself. Every property is read through its propstat status — BlueMind answers 207 with failing propstats where a naive client reads 2xx as success — and MKCALENDAR answers a raw MkCalendarResult whose one trustworthy confirmation is the caller's own fresh listing.
  • Method Details

    • endpoint

      CalDavEndpoint endpoint(Long serverId, String exoLogin)
      Mints the one kind of target this client will talk to: the account base of a declared server, resolved exclusively from the server registry — the row the account references, else the seed row, else the legacy deployment property — with the {username} placeholder replaced by the account's DAV username, percent-encoded for its path position.

      The caller names an eXo user, never a DAV account: the account segment of the URL comes from the credentials provider the server is configured with. Which matters as soon as a provider authenticates as one identity and addresses another — a technical account acting for a user. The account the URL names and the credentials the header carries are then two halves of one answer, and asking the provider for both is what keeps them from drifting into a valid session pointed at somebody else's calendar.

      Parameters:
      serverId - registry row the user's account references, or null for the seed-row / legacy-property resolution
      exoLogin - the eXo login of the user this conversation is for
      Returns:
      the endpoint every other method addresses
      Throws:
      CalDavException - when no server is declared anywhere, the resolved URL is unusable, or no account name can be resolved for a URL that needs one
    • discoverPrincipal

      String discoverPrincipal(CalDavEndpoint endpoint)
      Asks the server who the authenticated user is (current-user-principal), as a path on this endpoint — the first hop of every discovery here, exposed on its own because one caller needs the principal itself and not what hangs off it.

      The last path segment is the server's own identifier for the account. On BlueMind it is the container uid the account's default calendar carries in its name — /dav/principals/__uids__/<uid>/ against calendar:Default:<uid> — which is how CaldavPushService tells the account's own calendar from a resource listed beside it (EXO-90225). Asked of the server rather than parsed out of any calendar's href, so the two names being compared come from two independent answers.

      A principal the server does not name may come back as null or blank rather than as an exception. The HTTP client throws, because an account whose server will not say who it is has nothing to discover from; an implementer that has no principal to give — a fake, a server spoken to by href alone — may answer null or blank instead, and the one caller that compares the answer treats either as a principal it cannot read and refuses, never guesses. The caller's guard is therefore reachable by contract, not a leftover.

      Parameters:
      endpoint - the declared server
      Returns:
      the principal's server-absolute raw path, or null or blank when the implementer names none
      Throws:
      CalDavAuthenticationException - when the credentials are refused
      CalDavException - when the server cannot be reached, or — in the HTTP client — names no current user
    • discoverCalendarHome

      String discoverCalendarHome(CalDavEndpoint endpoint)
      Walks discovery from the endpoint base: asks who the authenticated user is (current-user-principal), then where that principal's calendars live (calendar-home-set). Started at the registered base path directly — never at /.well-known/caldav, whose contract is a redirect and this client follows none.
      Parameters:
      endpoint - the declared server to discover on
      Returns:
      the calendar home's server-absolute raw path
      Throws:
      CalDavAuthenticationException - when the credentials are refused
      CalDavException - when the server cannot be reached or answers no principal or no home
    • discoverHome

      CalendarHome discoverHome(CalDavEndpoint endpoint)
      The same walk as discoverCalendarHome(org.exoplatform.caldav.client.CalDavEndpoint), answering the principal it passed through beside the home it arrived at.

      The listing that follows compares each collection's DAV:owner against the account's own principal to tell the user's calendars from one a colleague shared with them (EXO-90235). Asking for the two in one walk is what keeps that comparison from costing a third PROPFIND on every pass, and keeps both answers from one conversation with the server. The principal obeys discoverPrincipal(org.exoplatform.caldav.client.CalDavEndpoint)'s contract: the HTTP client throws when the server names none, an implementer with none to give may answer null, and the caller treats a null as "cannot compare" rather than as anybody in particular.

      Parameters:
      endpoint - the declared server to discover on
      Returns:
      the principal and the calendar home, both as server-absolute raw paths
      Throws:
      CalDavAuthenticationException - when the credentials are refused
      CalDavException - when the server cannot be reached or answers no principal or no home
    • discoverDefaultCalendar

      String discoverDefaultCalendar(CalDavEndpoint endpoint)
      Asks the account which of its calendars it treats as the default one — the collection a scheduling client files an invitation into when nobody says otherwise.

      Two hops, both of them questions rather than guesses: the principal names its scheduling inbox (schedule-inbox-URL), and the inbox names the default calendar (schedule-default-calendar-URL) — the RFC 6638 property that exists precisely so a client does not have to invent one. Guessing from the home listing instead (the first collection, the one called "Calendar", the one whose path ends in the username) is what this method exists not to do: each of those is right on one server and wrong on the next, and being wrong means writing somebody's meetings into a calendar nobody was looking at.

      A server naming none is an answer, not a fault. Scheduling is an extension, and plenty of CalDAV servers implement none of it; that is a null here rather than an exception, and the caller decides what to do with a destination it cannot establish. Credentials being refused, or the server being unreachable, still raise — those are not "there is no default calendar".

      The path it answers is a claim, in the sense mkCalendar(org.exoplatform.caldav.client.CalDavEndpoint, java.lang.String, java.lang.String, java.lang.String) documents: the collection is only real once the caller has seen it in a listing of the account's own home.

      Parameters:
      endpoint - the declared server
      Returns:
      the default calendar's server-absolute raw path, or null when the account names none
      Throws:
      CalDavAuthenticationException - when the credentials are refused
      CalDavException - when the server cannot be reached or answers no principal at all
    • listCalendars

      List<CalendarCollection> listCalendars(CalDavEndpoint endpoint, String homeHref)
      Lists the calendar collections of a home with every property the sync engine binds on: display name, resource type, ctag, sync token, supported reports, the user's privileges, the collection's owner, and the calendar colour.
      Parameters:
      endpoint - the declared server
      homeHref - the calendar home's server-absolute path
      Returns:
      the calendar collections, never null — non-calendar members of the home (the home itself included) are filtered out here by resource type, never guessed from the path
      Throws:
      CalDavAuthenticationException - when the credentials are refused
      CalDavException - when the server cannot be reached or errors
    • readCalendar

      CalendarCollection readCalendar(CalDavEndpoint endpoint, String href)
      Reads one collection's properties at Depth:0 — what a bound pair re-reads on each run without paying a full home listing.
      Parameters:
      endpoint - the declared server
      href - the collection's server-absolute path
      Returns:
      the collection, or null when the resource exists but is not a calendar collection
      Throws:
      CalDavAuthenticationException - when the credentials are refused
      CalDavException - when the server cannot be reached or errors
    • readDisplayName

      String readDisplayName(CalDavEndpoint endpoint, String href)
      What one resource calls itself: its DAV:displayname, read with a single PROPFIND of depth 0.

      Asked of an owner principal, so that a calendar a colleague shared can be listed under the colleague's name rather than under their login (EXO-90237): Stalwart answers "Alice" for /dav/pal/alice%40stalwart.local/. The href is addressed like every other — through the endpoint's own authority and no other, the same refusal that keeps credentials off a host nobody declared — and a property the server withholds, or answers outside a granted propstat, is simply not there.

      Parameters:
      endpoint - the account's endpoint
      href - the resource's server-absolute path
      Returns:
      the display name, or null when the server states none
      Throws:
      CalDavAuthenticationException - when the credentials are refused
      CalDavException - when the server cannot be reached, refuses the read, or the href names another host
    • getCtag

      String getCtag(CalDavEndpoint endpoint, String href)
      Reads the collection's current ctag, the cheap tier-2 question asked first on every run.
      Parameters:
      endpoint - the declared server
      href - the collection's server-absolute path
      Returns:
      the current ctag, or null when the server does not implement it
      Throws:
      CalDavAuthenticationException - when the credentials are refused
      CalDavException - when the server cannot be reached or errors
    • listResourceEtags

      Map<String,String> listResourceEtags(CalDavEndpoint endpoint, String collectionHref)
      Lists every object in the collection with its version — the listResourceEtags pattern the plan reuses verbatim from email-connector, and what tells the engine who is new, who changed and who is gone. Also the mirror-verification listing of the metadata design.
      Parameters:
      endpoint - the declared server
      collectionHref - the collection's server-absolute path
      Returns:
      object path to object version, insertion-ordered, never null — the collection itself and any member without an ETag are skipped by that one rule, never by guessing from the path
      Throws:
      CalDavAuthenticationException - when the credentials are refused
      CalDavException - when the server cannot be reached or errors
    • calendarQuery

      List<CalendarObject> calendarQuery(CalDavEndpoint endpoint, String collectionHref, Instant start, Instant end)
      Runs a calendar-query REPORT for the VEVENTs of a time window — the tier-3 floor for servers whose listings carry no usable ETags, and the ranged read the events endpoint will serve from.
      Parameters:
      endpoint - the declared server
      collectionHref - the collection's server-absolute path
      start - window start, inclusive, or null for an unbounded start
      end - window end, exclusive, or null for an unbounded end
      Returns:
      the matching objects with their calendar data, never null
      Throws:
      CalDavAuthenticationException - when the credentials are refused
      CalDavException - when the server cannot be reached or errors
    • multiget

      List<CalendarObject> multiget(CalDavEndpoint endpoint, String collectionHref, List<String> hrefs)
      Fetches the calendar data of the given objects in one calendar-multiget REPORT — why a changed collection costs one round trip per batch rather than one per event.
      Parameters:
      endpoint - the declared server
      collectionHref - the collection's server-absolute path
      hrefs - the object paths to fetch
      Returns:
      the objects the server returned, never null; an href it did not return is simply absent, which the caller must tolerate
      Throws:
      CalDavAuthenticationException - when the credentials are refused
      CalDavException - when the server cannot be reached or errors
    • syncCollection

      SyncCollectionResult syncCollection(CalDavEndpoint endpoint, String collectionHref, String syncToken)
      Runs an RFC 6578 sync-collection REPORT with a stored token — tier 1, one round trip for everything changed or deleted since that token.
      Parameters:
      endpoint - the declared server
      collectionHref - the collection's server-absolute path
      syncToken - the stored token, or null/blank for the initial sync that establishes one
      Returns:
      the changes and the token to store — or the invalid-token result when the server rejected the presented token, which the caller answers by falling through to the listing tier for this run
      Throws:
      CalDavAuthenticationException - when the credentials are refused
      CalDavException - when the server cannot be reached or errors
    • probeCapabilities

      ServerCapabilities probeCapabilities(CalDavEndpoint endpoint, String collectionHref)
      Probes which report tiers a collection's server can serve, in one Depth:0 PROPFIND. Stateless: the answer is bind-time information whose caching belongs to the caller's pair state, not to this client.
      Parameters:
      endpoint - the declared server
      collectionHref - the collection's server-absolute path
      Returns:
      the capability answer, never null
      Throws:
      CalDavAuthenticationException - when the credentials are refused
      CalDavException - when the server cannot be reached or errors
    • fetchObject

      CalendarObject fetchObject(CalDavEndpoint endpoint, String href)
      Reads one object's current data and version in a single GET — what a conflict re-read asks, so the three-way runs against the object as the server holds it NOW rather than as the last sync remembers it.
      Parameters:
      endpoint - the declared server
      href - the object's server-absolute path
      Returns:
      the object as the server returned it, or null when the server says there is no such object — including BlueMind's way of saying it, a 500 for an .ics that is simply not there; reporting "absent" on a server error is safe rather than optimistic because every creating write keeps its If-None-Match: *, so the worst case is a refused write, never a silently overwritten one
      Throws:
      CalDavAuthenticationException - when the credentials are refused
      CalDavException - when the server cannot be reached or errors
    • putObject

      PutResult putObject(CalDavEndpoint endpoint, String href, String icsData)
      Creates one object — always under If-None-Match: *, with no unconditional variant on purpose: the server itself then guarantees the PUT can only CREATE, and an object already at that path answers 412 instead of being overwritten, whatever race led there.
      Parameters:
      endpoint - the declared server
      href - the object's server-absolute path to create at
      icsData - the iCalendar text to store
      Returns:
      the status and, when the server sent them, the stored version and the object's real location; a 412 is answered, not thrown
      Throws:
      CalDavAuthenticationException - when the credentials are refused
      CalDavException - when the server cannot be reached or answers a status that is neither a write nor a precondition refusal
    • overwriteObject

      PutResult overwriteObject(CalDavEndpoint endpoint, String href, String icsData)
      Writes one object over whatever is there, with no precondition at all.

      The deliberate exception to the conditional-write doctrine every other method here follows, and the only one. putObject(org.exoplatform.caldav.client.CalDavEndpoint, java.lang.String, java.lang.String) sends If-None-Match: * and so refuses an object that exists; updateObject(org.exoplatform.caldav.client.CalDavEndpoint, java.lang.String, java.lang.String, java.lang.String) sends If-Match and so refuses an object that has moved on. A repair needs to write in exactly the case both of them refuse — the copy is there and it has drifted — so neither can carry it, and the guard has to come off.

      What makes that safe is not this method but its caller: a repair is only reached after both copies have been read and compared and the eXo one judged authoritative. Called anywhere that comparison has not happened, this silently destroys somebody's change. There is no way for the client to check that, which is why the name says overwrite.

      Parameters:
      endpoint - the account's endpoint
      href - the object's path
      icsData - the iCalendar object to write
      Returns:
      the result, which cannot be a precondition failure
    • updateObject

      PutResult updateObject(CalDavEndpoint endpoint, String href, String icsData, String ifMatch)
    • deleteObject

      int deleteObject(CalDavEndpoint endpoint, String href, String ifMatch)
      Deletes one object, conditionally when a version is given.
      Parameters:
      endpoint - the declared server
      href - the object's server-absolute path
      ifMatch - the If-Match value to guard the delete with, or null to delete unconditionally — tolerated here (unlike updateObject(org.exoplatform.caldav.client.CalDavEndpoint, java.lang.String, java.lang.String, java.lang.String)) because a delete of an already-diverged object is recoverable where an overwrite is not, and some flows legitimately hold no version
      Returns:
      the status the server answered: 200/204 deleted, 404/410 already gone — a fact, not a fault — and 412 a refused precondition
      Throws:
      CalDavAuthenticationException - when the credentials are refused
      CalDavException - when the server cannot be reached or answers any other status
    • mkCalendar

      MkCalendarResult mkCalendar(CalDavEndpoint endpoint, String href, String displayName, String color)
      Asks the server to create a calendar collection, display name and optional colour set atomically. The request always declares supported-calendar-component-set with VEVENT: BlueMind derives the created collection's kind from it, and without it answers 201 while creating nothing (proven live 2026-08-20). The answer is a claim, never a fact — see MkCalendarResult for why the one statement of success a caller may trust is the collection's presence in its own fresh listing.
      Parameters:
      endpoint - the declared server
      href - the collection's server-absolute path to create at
      displayName - the display name to create with
      color - the Apple calendar-color to set, or null to set none
      Returns:
      the raw outcome, refusals included — a server declining MKCALENDAR gives an answer the caller maps to its degraded states, not an error
      Throws:
      CalDavAuthenticationException - when the credentials are refused (401/407 — a 403 here is the refusal, not an auth failure)
      CalDavException - when the server cannot be reached
    • setDisplayName

      PropPatchResult setDisplayName(CalDavEndpoint endpoint, String href, String displayName)
      Asks the server to change the display name of an existing collection — a PROPPATCH of DAV:displayname, the one property eXo propagates after creation. The name travels as data and never as identity: the collection stays where its path put it, and only what the user reads in their own client changes.

      As with mkCalendar(org.exoplatform.caldav.client.CalDavEndpoint, java.lang.String, java.lang.String, java.lang.String), the answer is a claim. The one statement of success a caller may trust is the name read back from the collection afterwards, through readCalendar(org.exoplatform.caldav.client.CalDavEndpoint, java.lang.String); see PropPatchResult.

      Parameters:
      endpoint - the declared server
      href - the collection's server-absolute path
      displayName - the name to set
      Returns:
      the raw outcome, refusals included — a server declining PROPPATCH gives an answer the caller logs and lives with, not an error
      Throws:
      CalDavAuthenticationException - when the credentials are refused (401/407 — a 403 here is the refusal, not an auth failure)
      CalDavException - when the server cannot be reached
    • deleteCollection

      int deleteCollection(CalDavEndpoint endpoint, org.exoplatform.caldav.model.CalendarSync pair)
      Removes a whole collection, and everything in it.

      The only irreversible outward-facing call in this client, and the one that deletes data eXo may not have authored — a user's own events, added from their phone, live in the collection too and go with it.

      It takes the pair rather than an href on purpose. An href parameter would let any caller name any collection, and the guard would then live in whoever remembered to write it. Taking the binding means the invariant is checked where the request is built: the collection must belong to an SyncOrigin.EXO pair, and its last path segment must be the one eXo derives from that pair's own calendar anchor. A collection eXo did not create cannot be addressed by this method at all.

      Parameters:
      endpoint - the declared server
      pair - the binding whose collection is to be removed
      Returns:
      the HTTP status; 404 and 410 mean it was already gone
      Throws:
      IllegalArgumentException - when the pair does not authorise the deletion — never a runtime refusal to be caught and worked around, but a programming error surfaced at the call site
      CalDavAuthenticationException - when the credentials are refused
      CalDavException - when the server refuses or cannot be reached
    • capabilities

      DavOptions capabilities(CalDavEndpoint endpoint, String href)
      What a resource supports: the compliance classes of its DAV header and the methods of its Allow header (EXO-90253).

      The evidence SharingMechanism.of(DavOptions, String) selects a granting mechanism from. It is asked of the collection itself, not of the server root, because RFC 4918 §10.1 lets the DAV header vary per resource.

      One OPTIONS comes first; Stalwart answers both headers there. When that answer carries no DAV header, the compliance classes come from the DAV header of a depth-0 PROPFIND of the same resource. This is the BlueMind case on the deployments observed (the rig on 2026-09-14, seen as nginx, and step 1 of dev/golden-capture/capture-bluemind.sh, an authenticated request): its OPTIONS came back as a bare 204 with neither header, answered in front of its DAV server, although that DAV server would set both headers itself (OptionsProtocol.write). That server sets DAV on every PROPFIND answer (PropFindProtocol.java:125; captured on its principal and calendar homes). The methods come from OPTIONS only, since a PROPFIND answer does not list them. A mechanism that needs a method — SharingMechanism.WEBDAV_ACL and its ACL — is therefore selected only on a server that lists it where methods are listed.

      Parameters:
      endpoint - the account's endpoint
      href - the resource's server-absolute path
      Returns:
      the normalised classes and methods, empty sets where neither answer carried them
      Throws:
      CalDavAuthenticationException - when the credentials are refused
      CalDavException - when the server cannot be reached, answers the OPTIONS with neither 200 nor 204, or refuses the PROPFIND
    • readAcl

      CollectionAcl readAcl(CalDavEndpoint endpoint, String href)
      Reads a collection's access control list and the caller's own privileges on it, in one PROPFIND of depth 0 (EXO-90253).

      The whole list or nothing usable: an entry outside RFC 3744 §5.5's grammar makes the answer not understood, because the list is read in order to be written back, and a skipped entry would be deleted by that write. A property the server does not grant — no DAV:read-acl, or no RFC 3744 at all — is not readable, not an exception.

      Parameters:
      endpoint - the account's endpoint
      href - the collection's server-absolute path
      Returns:
      the list as the server answered it
      Throws:
      CalDavAuthenticationException - when the credentials are refused
      CalDavException - when the server cannot be reached, errors, or answers something that is not DAV XML
    • writeAcl

      AclWriteResult writeAcl(CalDavEndpoint endpoint, org.exoplatform.caldav.model.CalendarSync pair, List<AccessControlEntry> entries)
      Replaces the modifiable entries of a collection's access control list with the given ones: RFC 3744 §8.1's ACL method (EXO-90253).

      Replaces, never adds. Every entry that is neither protected nor inherited and is absent from the request is removed by the server. The caller therefore sends the list it read with its one change applied, and this method refuses an entry that is protected or inherited rather than sending what the server would refuse as a conflict.

      Takes the pair, not a path, so that it cannot address anything a browser named. The collection must belong to an SyncOrigin.EXO pair and carry the slug eXo derives from that pair's calendar anchor, or to an active, anchored SyncOrigin.REMOTE pair that is not the meetings mirror — an imported calendar, whose ownership on the server the share service confirms before calling. It never addresses the mirror, a hidden share, or a collection bound to no pair. deleteCollection(org.exoplatform.caldav.client.CalDavEndpoint, org.exoplatform.caldav.model.CalendarSync) keeps the stricter eXo-created rule.

      The answer is a claim: AclWriteResult.accepted() says the server took the body, and only the list read back says what it holds.

      Parameters:
      endpoint - the account's endpoint
      pair - the binding whose collection's ACL is written
      entries - the complete list of modifiable entries to hold
      Returns:
      the status and, for a refusal, the preconditions it named
      Throws:
      IllegalArgumentException - when the pair does not authorise addressing its collection, or an entry is protected, inherited or not representable
      CalDavAuthenticationException - when the credentials are refused
      CalDavException - when the server cannot be reached
    • readCalendarUserAddresses

      List<String> readCalendarUserAddresses(CalDavEndpoint endpoint, String principalHref)
      The addresses a principal publishes as a calendar user (CALDAV:calendar-user-address-set, RFC 6638 §2.4.1), read with a PROPFIND of depth 0 through this endpoint (EXO-90253).

      On BlueMind the set holds the principal path, a urn:uuid: and mailto: followed by the directory entry's e-mail — the one column BlueMind's share handler looks a sharee up by (plugins/net.bluemind.dav.server/.../props/caldav/CalendarUserAddressSet.java, DirEntryStore.java dir.email ilike).

      Parameters:
      endpoint - the endpoint the question is asked through
      principalHref - the principal's server-absolute path
      Returns:
      every href of the set in order, empty when the server states none
      Throws:
      CalDavAuthenticationException - when the credentials are refused
      CalDavException - when the server cannot be reached or errors
    • postCalendarServerShare

      int postCalendarServerShare(CalDavEndpoint endpoint, org.exoplatform.caldav.model.CalendarSync pair, String address, boolean remove)
      Shares a collection with one sharee, read-only, or stops sharing it, with Apple's POST CS:share (EXO-90253).

      Takes the pair, like writeAcl(org.exoplatform.caldav.client.CalDavEndpoint, org.exoplatform.caldav.model.CalendarSync, java.util.List<org.exoplatform.caldav.client.AccessControlEntry>), under the same rule: a collection eXo created for the pair's calendar, or an active imported one that is not the meetings mirror. The sharee is named by a mail address, sent as mailto:; only reading is ever granted.

      The answer is a claim and, on BlueMind, not even that: its handler swallows every failure and answers 200 (SharingProtocol.java, VEventStuffPostProtocol.java). The caller confirms the change by reading the access list back.

      Parameters:
      endpoint - the owner's endpoint
      pair - the binding whose collection is shared
      address - the sharee's mail address, without mailto:
      remove - true to stop sharing, false to share read-only
      Returns:
      the 2xx status the server answered
      Throws:
      IllegalArgumentException - when the pair does not authorise addressing its collection, or the address is not a mail address
      CalDavAuthenticationException - when the credentials are refused
      CalDavForbiddenException - when the server refuses with 403
      CalDavException - when the server cannot be reached or answers anything else