Class EventIcsBuilder
eXo describes one meeting as iCalendar through two channels, and they must
not drift apart again. The mail channel attaches a document to the
notification that announces an event (Utils.generateIcsFile(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.util.Locale, java.time.ZoneId)); the
calendar copy channel pushes an object into the user's own calendar
over CalDAV (the caldav-integration addon). They were written separately,
they diverged, and five payload defects fixed in one of them had to be
looked for by hand in the other. This class is where the answer to "do they
say the same thing?" stops depending on anybody remembering to look.
The two outputs are deliberately not the same document, and this class
does not try to make them one. The mailed document carries no attendees at
all — answering happens through the tokenised links in the mail body, not
through iMIP (EXO-89705) — while the copy must carry them with their
PARTSTAT, because a roster the client recognises is the only thing that
makes it offer RSVP (EXO-89681). What is shared is the core: what the
meeting is called, what it is about, where it came from, and — since
EXO-89751 — where it lives, the URL both channels now write and
both channels name in the description. What stays with
each channel is who is written into it and how it is to be acted on:
attendees and PARTSTAT, SCHEDULE-AGENT, alarms, STATUS and TRANSP, and
METHOD — which the mail declares as PUBLISH and which a calendar object
resource must not declare at all (RFC 4791 §4.1).
The description is the piece that matters most here, and the reason is worth stating. The mail said "Invitation sent by X in space Chemistry"; the copy said nothing at all — no attribution, often no DESCRIPTION whatsoever. That is backwards. A reader opening the mail already knows where they are; the copy sits in their calendar among fifty other entries with nothing to say which system put it there or which space it belongs to. Written once, here, both channels say it.
This class lives in agenda rather than in caldav-integration because the dependency runs one way only: caldav-integration builds on agenda and never the reverse. caldav-integration passes in what only it knows.
The text is written once, whatever the description already carries.
A description handed in can itself begin with this builder's text: the
CalDAV connector stores the whole description of a copy it imports, and on
an account two eXo users share, the copy one of them pushed is what the
other imports. Prepending unconditionally then stacked one block per edit
round trip (EXO-90227). Both flavours below take any such block off the
description before writing their own — InvitationText says how it
is recognised, and why it is replaced rather than kept.
-
Method Summary
Modifier and TypeMethodDescriptionstatic URIcalendarUserAddress(String email) Turns a bare mail address into the CAL-ADDRESS URI RFC 5545 §3.3.3 requires for ORGANIZER and ATTENDEE.static Stringdescription(Locale userLocale, String eventCreatorFullName, String spaceName, String conferenceUrl, String eventUrl, Map<org.exoplatform.agenda.constant.EventAttendeeResponse, String> rsvpLinks, String eventDescriptionHtml) The plain-text DESCRIPTION both channels write, attribution included.static StringeventUrl(long eventId) The link that opens this event in eXo, derived from the event itself.static StringhtmlDescription(Locale userLocale, String eventCreatorFullName, String spaceName, String conferenceUrl, String eventUrl, String eventDescriptionHtml) The HTML flavour of the same blurb, the one X-ALT-DESC carries.static StringhtmlToPlainText(String html) Renders an HTML fragment as the plain text a calendar client would show.
-
Method Details
-
eventUrl
The link that opens this event in eXo, derived from the event itself.Derived, never passed in. The CalDAV channel used to take this value from whatever the browser had put on the push request, which meant only a browser push carried it: every sweep and every repair rendered the copy with no
URLat all, so the link appeared once and the next repair stripped it — and the mirror comparison had to ignore the property outright, because comparing a value one side never renders makes every copy look rewritten. Deriving it here is what fixes both: the same event renders the same string on a browser push, on a sweep, on a repair and in the mailed document, so there is nothing left to lose and nothing left to exempt (EXO-89751, EXO-89716).The shape is not invented here. It is
NotificationUtils.getEventURL(long)— the one already in the body of every notification mail about this event, and the sameagenda?eventId=route the browser was building for itself.It is guarded like
label(java.util.Locale, java.lang.String), and for the same reason: the address needs the portal's configured domain and its meta portal, neither of which is resolvable outside a running container. A copy that reached nobody's calendar because a link could not be composed would be a worse outcome than a copy without a link, and this builder is on the push path.- Parameters:
eventId- technical identifier of the event, 0 or less when the caller has none- Returns:
- the absolute link, or null when there is no event to name or the portal cannot be asked
-
description
public static String description(Locale userLocale, String eventCreatorFullName, String spaceName, String conferenceUrl, String eventUrl, Map<org.exoplatform.agenda.constant.EventAttendeeResponse, String> rsvpLinks, String eventDescriptionHtml) The plain-text DESCRIPTION both channels write, attribution included.It is built from the raw labels and values rather than by unescaping the HTML flavour below. Going through the HTML runs every label through
HTMLEntityEncoder, so a French label arrives asenvoyée, whose semicolon the iCalendar writer then escapes again intoenvoyé\;e— an accented word mangled twice over. Reading from the source instead means the value is plain UTF-8 whatever the locale.DESCRIPTION is plain text by definition (RFC 5545 §3.8.1.5); the HTML flavour belongs to X-ALT-DESC alone, which is why the two are built by two methods sitting next to each other rather than by one deriving the other.
- Parameters:
userLocale- locale the labels are read in; the platform default when nulleventCreatorFullName- display name of whoever called the meeting, blank when it cannot be resolvedspaceName- display name of the space the event belongs to, blank for an event that belongs to no spaceconferenceUrl- conference link, blank when the event has noneeventUrl- link back to the event in eXo, fromeventUrl(long); blank deliberately for a recipient who has no eXo account, for whom it resolves to a login screen (EXO-89751)rsvpLinks- the answer links to offer, keyed by the answer they record, built for the one recipient this document is for; null or empty for a channel that offers no answer links, which is every channel except the calendar copy (EXO-89753)eventDescriptionHtml- the event's own description, HTML as the editor stored it, blank when the event has none; a description that already begins with this builder's text contributes only what follows that text (EXO-90227)- Returns:
- the description as plain text, with real line breaks
-
htmlDescription
public static String htmlDescription(Locale userLocale, String eventCreatorFullName, String spaceName, String conferenceUrl, String eventUrl, String eventDescriptionHtml) The HTML flavour of the same blurb, the one X-ALT-DESC carries.Only the mail channel writes it: a CalDAV object resource has no use for a non-standard alternate description, and the clients that read a copy show DESCRIPTION. It lives here beside its plain-text twin all the same, because the two are two renderings of one text and separating them is how they drift.
It takes no answer links, and that is not an omission. Only the calendar copy carries them (EXO-89753), for a reason specific to it: its client may offer no RSVP control of its own. A mail already carries its Accept and Decline buttons in the body, and the mailed document is a PUBLISH with no attendees on it. Adding a parameter here that every caller would pass as null would be a signature saying something the class does not do.
- Parameters:
userLocale- locale the labels are read in; the platform default when nulleventCreatorFullName- display name of whoever called the meetingspaceName- display name of the space, blank for noneconferenceUrl- conference link, blank when the event has noneeventUrl- link back to the event in eXo, blank for a recipient with no eXo accounteventDescriptionHtml- the event's own description as HTML, blank when the event has none; a description that already begins with this builder's text contributes only what follows that text (EXO-90227)- Returns:
- the description as an HTML document, on a single line
-
calendarUserAddress
Turns a bare mail address into the CAL-ADDRESS URI RFC 5545 §3.3.3 requires for ORGANIZER and ATTENDEE.A bare address is not a URI: it has no scheme, so a client that validates the value simply drops the property — and with ORGANIZER goes any attribution of the invitation, while with ATTENDEE goes the roster that is the only reason a client offers RSVP on a copy.
An address that already carries the scheme is left alone. That case is not hypothetical on the copy channel: the address written for the account's own owner is whatever their CalDAV account answers to, which is configuration rather than a profile field, and a value pasted in with its scheme would otherwise become
mailto:mailto:....- Parameters:
email- mail address, already known to be non blank- Returns:
- the same address as a
mailto:URI, left untouched when it already carries that scheme
-
htmlToPlainText
Renders an HTML fragment as the plain text a calendar client would show.Block boundaries and
<br>become real line breaks, every other tag is dropped and every entity is decoded, so an accented character written asécomes back as the character itself.- Parameters:
html- HTML fragment, blank tolerated- Returns:
- the fragment as plain text, empty when there was nothing to render
-