Class EventIcsBuilder

java.lang.Object
org.exoplatform.agenda.util.EventIcsBuilder

public final class EventIcsBuilder extends Object
What every iCalendar document eXo writes about a meeting says the same way.

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.

  • Method Details

    • eventUrl

      public static String eventUrl(long eventId)
      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 URL at 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 same agenda?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 as envoy&eacute;e, whose semicolon the iCalendar writer then escapes again into envoy&eacute\;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 null
      eventCreatorFullName - display name of whoever called the meeting, blank when it cannot be resolved
      spaceName - display name of the space the event belongs to, blank for an event that belongs to no space
      conferenceUrl - conference link, blank when the event has none
      eventUrl - link back to the event in eXo, from eventUrl(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
      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 null
      eventCreatorFullName - display name of whoever called the meeting
      spaceName - display name of the space, blank for none
      conferenceUrl - conference link, blank when the event has none
      eventUrl - link back to the event in eXo, blank for a recipient with no eXo account
      eventDescriptionHtml - the event's own description as HTML, blank when the event has none
      Returns:
      the description as an HTML document, on a single line
    • calendarUserAddress

      public static URI calendarUserAddress(String email)
      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

      public static String htmlToPlainText(String html)
      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 &eacute; 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