Class HttpCalDavClient

java.lang.Object
org.exoplatform.caldav.client.HttpCalDavClient
All Implemented Interfaces:
CalDavClient

@Component public class HttpCalDavClient extends Object implements CalDavClient
CalDAV over the JDK's own HTTP client, modelled line for line on email-connector's HttpCardDavClient — the hand-written client that already works against the same BlueMind that defeated the browser — with the disciplines the relay established carried over:
  • registry-only targets — endpoints are minted from the server registry alone (endpoint(Long, String)), hrefs resolve against that endpoint's authority, and a foreign-host href is refused;
  • no redirects — a declared server must not be able to bounce this client's credentialed requests to another host, so the transport never follows a 3xx (which also rules the well-known discovery flow out by design: discovery starts at the registered base path instead);
  • explicit timeouts and a bounded body read, both deployment-tunable;
  • no auth material anywhere near a log or an exception — every message names the method and the URI, never a header.
The XML is parsed with doctypes and external entities disabled: the payload comes from whatever server an administrator declared, which is still attacker-influenced input, and a stock parser would happily read files off this machine on its behalf. And every property is read through its propstat status: BlueMind answers 207 with failing propstats where a naive client reads 2xx as success — the false-success class that shipped a bug the week this client was designed.
  • Constructor Details

    • HttpCalDavClient

      @Autowired public HttpCalDavClient(CaldavServerService caldavServerService, CaldavCredentialsResolver caldavCredentialsResolver)
      The client Spring builds. Redirects are NEVER followed: the target host comes from the registry, and a compromised or misconfigured registered server must not be able to bounce credentialed requests anywhere else.
      Parameters:
      caldavServerService - the registry every endpoint is resolved from
      caldavCredentialsResolver - the addon's seam onto the shared credentials contract — this client resolves, it never assembles
  • Method Details

    • endpoint

      public CalDavEndpoint endpoint(Long serverId, String exoLogin)
      Description copied from interface: CalDavClient
      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.

      Specified by:
      endpoint in interface CalDavClient
      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
    • discoverCalendarHome

      public String discoverCalendarHome(CalDavEndpoint endpoint)
      The home half of discoverHome(org.exoplatform.caldav.client.CalDavEndpoint), kept for the callers that need where the calendars are and never compare owners — the outbound binding, the mirror lookup, the deletion path. One walk, two views of its answer.
      Specified by:
      discoverCalendarHome in interface CalDavClient
      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

      public CalendarHome discoverHome(CalDavEndpoint endpoint)
      The same two hops as discoverCalendarHome(org.exoplatform.caldav.client.CalDavEndpoint), keeping the principal the first one answered instead of throwing it away — the walk the sync and the read-through run, so that the owner of each listed collection can be compared against the account's own principal without a third PROPFIND (EXO-90235).
      Specified by:
      discoverHome in interface CalDavClient
      Parameters:
      endpoint - the declared server to discover on
      Returns:
      the principal and the 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
    • discoverPrincipal

      public String discoverPrincipal(CalDavEndpoint endpoint)
      Who the server says is speaking, as a path on this endpoint - the first hop of every discovery, its own method because two of them start there, and on the interface because the push service compares its last segment with the name of a listed calendar (EXO-90225).
      Specified by:
      discoverPrincipal in interface CalDavClient
      Parameters:
      endpoint - the account's endpoint
      Returns:
      the principal's path
      Throws:
      CalDavException - when the server names no current user
    • discoverDefaultCalendar

      public String discoverDefaultCalendar(CalDavEndpoint endpoint)
      Description copied from interface: CalDavClient
      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 CalDavClient.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.

      Specified by:
      discoverDefaultCalendar in interface CalDavClient
      Parameters:
      endpoint - the declared server
      Returns:
      the default calendar's server-absolute raw path, or null when the account names none
    • listCalendars

      public List<CalendarCollection> listCalendars(CalDavEndpoint endpoint, String homeHref)
      Description copied from interface: CalDavClient
      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.
      Specified by:
      listCalendars in interface CalDavClient
      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
    • readCalendar

      public CalendarCollection readCalendar(CalDavEndpoint endpoint, String href)
      Description copied from interface: CalDavClient
      Reads one collection's properties at Depth:0 — what a bound pair re-reads on each run without paying a full home listing.
      Specified by:
      readCalendar in interface CalDavClient
      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
    • readDisplayName

      public String readDisplayName(CalDavEndpoint endpoint, String href)
      One PROPFIND of depth 0 for the one property, through propfind(org.exoplatform.caldav.client.CalDavEndpoint, java.lang.String, java.lang.String, java.lang.String) and so through target(org.exoplatform.caldav.client.CalDavEndpoint, java.lang.String): a principal path naming another authority is refused before a socket is opened, exactly as any other href would be. Read from a granted propstat only, like every property this client answers — a server that does not know the resource answers the name in a 404 propstat, and that is no name.
      Specified by:
      readDisplayName in interface CalDavClient
      Parameters:
      endpoint - the account's endpoint
      href - the resource's server-absolute path
      Returns:
      the display name, or null when the server states none
    • getCtag

      public String getCtag(CalDavEndpoint endpoint, String href)
      Description copied from interface: CalDavClient
      Reads the collection's current ctag, the cheap tier-2 question asked first on every run.
      Specified by:
      getCtag in interface CalDavClient
      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
    • listResourceEtags

      public Map<String,String> listResourceEtags(CalDavEndpoint endpoint, String collectionHref)
      Description copied from interface: CalDavClient
      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.
      Specified by:
      listResourceEtags in interface CalDavClient
      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
    • calendarQuery

      public List<CalendarObject> calendarQuery(CalDavEndpoint endpoint, String collectionHref, Instant start, Instant end)
      Description copied from interface: CalDavClient
      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.
      Specified by:
      calendarQuery in interface CalDavClient
      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
    • multiget

      public List<CalendarObject> multiget(CalDavEndpoint endpoint, String collectionHref, List<String> hrefs)
      Description copied from interface: CalDavClient
      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.
      Specified by:
      multiget in interface CalDavClient
      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
    • syncCollection

      public SyncCollectionResult syncCollection(CalDavEndpoint endpoint, String collectionHref, String syncToken)
      Description copied from interface: CalDavClient
      Runs an RFC 6578 sync-collection REPORT with a stored token — tier 1, one round trip for everything changed or deleted since that token.
      Specified by:
      syncCollection in interface CalDavClient
      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
    • probeCapabilities

      public ServerCapabilities probeCapabilities(CalDavEndpoint endpoint, String collectionHref)
      Description copied from interface: CalDavClient
      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.
      Specified by:
      probeCapabilities in interface CalDavClient
      Parameters:
      endpoint - the declared server
      collectionHref - the collection's server-absolute path
      Returns:
      the capability answer, never null
    • fetchObject

      public CalendarObject fetchObject(CalDavEndpoint endpoint, String href)
      Description copied from interface: CalDavClient
      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.
      Specified by:
      fetchObject in interface CalDavClient
      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
    • putObject

      public PutResult putObject(CalDavEndpoint endpoint, String href, String icsData)
      Description copied from interface: CalDavClient
      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.
      Specified by:
      putObject in interface CalDavClient
      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
    • overwriteObject

      public PutResult overwriteObject(CalDavEndpoint endpoint, String href, String icsData)
      Description copied from interface: CalDavClient
      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. CalDavClient.putObject(org.exoplatform.caldav.client.CalDavEndpoint, java.lang.String, java.lang.String) sends If-None-Match: * and so refuses an object that exists; CalDavClient.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.

      Specified by:
      overwriteObject in interface CalDavClient
      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

      public PutResult updateObject(CalDavEndpoint endpoint, String href, String icsData, String ifMatch)
      Specified by:
      updateObject in interface CalDavClient
    • deleteObject

      public int deleteObject(CalDavEndpoint endpoint, String href, String ifMatch)
      Description copied from interface: CalDavClient
      Deletes one object, conditionally when a version is given.
      Specified by:
      deleteObject in interface CalDavClient
      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 CalDavClient.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
    • mkCalendar

      public MkCalendarResult mkCalendar(CalDavEndpoint endpoint, String href, String displayName, String color)
      Description copied from interface: CalDavClient
      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.
      Specified by:
      mkCalendar in interface CalDavClient
      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
    • setDisplayName

      public PropPatchResult setDisplayName(CalDavEndpoint endpoint, String href, String displayName)
      Description copied from interface: CalDavClient
      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 CalDavClient.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 CalDavClient.readCalendar(org.exoplatform.caldav.client.CalDavEndpoint, java.lang.String); see PropPatchResult.

      Specified by:
      setDisplayName in interface CalDavClient
      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
    • capabilities

      public DavOptions capabilities(CalDavEndpoint endpoint, String href)
      One OPTIONS, and one depth-0 PROPFIND of the same resource when that OPTIONS carries no DAV header. Both go through the read-verb status policy: they change nothing, and on BlueMind a 403 to a read is a credential refusal. The Allow methods are taken from the OPTIONS alone.
      Specified by:
      capabilities in interface CalDavClient
      Parameters:
      endpoint - the account's endpoint
      href - the resource's server-absolute path
      Returns:
      the normalised DAV classes and Allow methods
    • readAcl

      public CollectionAcl readAcl(CalDavEndpoint endpoint, String href)
      One PROPFIND of depth 0, every entry parsed or the whole list declared not understood. Read from a granted propstat only, like every property here: Stalwart answers an owner's list as a granted, possibly empty DAV:acl, and a server that withholds it answers it in a 403 or 404 propstat, which is no list.
      Specified by:
      readAcl in interface CalDavClient
      Parameters:
      endpoint - the account's endpoint
      href - the collection's server-absolute path
      Returns:
      the list as answered
    • writeAcl

      public AclWriteResult writeAcl(CalDavEndpoint endpoint, org.exoplatform.caldav.model.CalendarSync pair, List<AccessControlEntry> entries)
      One ACL request carrying the complete list, addressed only through the pair's authorised collection.

      Credentials are classified on 401 and 407 only: a 403 to this write verb is the server declining the change — need-privileges, allowed-principal — with credentials that are fine, and pausing the account over it would be wrong. A gateway status means nothing was reached. Every other status is answered, with what its body names.

      Specified by:
      writeAcl in interface CalDavClient
      Parameters:
      endpoint - the account's endpoint
      pair - the binding whose collection is addressed
      entries - the complete list of modifiable entries
      Returns:
      the raw outcome
    • readCalendarUserAddresses

      public List<String> readCalendarUserAddresses(CalDavEndpoint endpoint, String principalHref)
      One PROPFIND of depth 0, every href of a granted calendar-user-address-set.
      Specified by:
      readCalendarUserAddresses in interface CalDavClient
      Parameters:
      endpoint - the endpoint asked through
      principalHref - the principal's path
      Returns:
      the hrefs, in order
    • postCalendarServerShare

      public int postCalendarServerShare(CalDavEndpoint endpoint, org.exoplatform.caldav.model.CalendarSync pair, String address, boolean remove)
      One POST CS:share to the pair's collection, as shareTarget(org.exoplatform.caldav.model.CalendarSync) admits it. Credentials are classified on 401 and 407 only; any other status outside 2xx is the server's refusal.
      Specified by:
      postCalendarServerShare in interface CalDavClient
      Parameters:
      endpoint - the owner's endpoint
      pair - the binding whose collection is addressed
      address - the sharee's mail address
      remove - whether to stop sharing
      Returns:
      the 2xx status
    • deleteCollection

      public int deleteCollection(CalDavEndpoint endpoint, org.exoplatform.caldav.model.CalendarSync pair)
      Description copied from interface: CalDavClient
      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.

      Specified by:
      deleteCollection in interface CalDavClient
      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