Repository: urlaubsverwaltung/urlaubsverwaltung
Pull request #4853 (2024-08-15 20:15:23)

Ical4j3 downgrade - all day events with timezone incorrect
<!--

Thanks for contributing to the Urlaubsverwaltung.
Please review the following notes before submitting you pull request.

Please look for other issues or pull requests which already work on this topic. Is somebody already on it? Do you need to synchronize?

# Security Vulnerabilities

🛑 STOP! 🛑 If your contribution fixes a security vulnerability, please do not submit it.
Instead, please open a report of a security vulnerability via
[Report a security vulnerability](https://github.com/urlaubsverwaltung/urlaubsverwaltung/security/advisories/new)


# Describing Your Changes

If, having reviewed the notes above, you're ready to submit your pull request, please
provide a brief description of the proposed changes.

If they:
 🐞 fix a bug, please describe the broken behaviour and how the changes fix it.
    Please label with 'type: bug' and 'status: new'
    
 🎁 make an enhancement, please describe the new functionality and why you believe it's useful.
    Please label with 'type: enhancement' and 'status: new'
 
If your pull request relates to any existing issues,
please reference them by using the issue number prefixed with #.

-->



Patch:
diff --git a/pom.xml b/pom.xml
index bcca0cbfe..a4735f18f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -179,7 +179,7 @@
     <dependency>
       <groupId>org.mnode.ical4j</groupId>
       <artifactId>ical4j</artifactId>
-      <version>4.0.2</version>
+      <version>3.2.18</version>
     </dependency>
 
     <!-- GOOGLE CALENDAR SYNC -->
diff --git a/src/main/java/org/synyx/urlaubsverwaltung/calendar/ICalService.java b/src/main/java/org/synyx/urlaubsverwaltung/calendar/ICalService.java
index 68b35fb8a..cef396e8c 100644
--- a/src/main/java/org/synyx/urlaubsverwaltung/calendar/ICalService.java
+++ b/src/main/java/org/synyx/urlaubsverwaltung/calendar/ICalService.java
@@ -2,11 +2,10 @@ package org.synyx.urlaubsverwaltung.calendar;
 
 import net.fortuna.ical4j.data.CalendarOutputter;
 import net.fortuna.ical4j.model.Calendar;
+import net.fortuna.ical4j.model.Date;
+import net.fortuna.ical4j.model.DateTime;
 import net.fortuna.ical4j.model.ParameterList;
-import net.fortuna.ical4j.model.TimeZoneRegistry;
-import net.fortuna.ical4j.model.TimeZoneRegistryFactory;
 import net.fortuna.ical4j.model.component.VEvent;
-import net.fortuna.ical4j.model.component.VTimeZone;
 import net.fortuna.ical4j.model.parameter.Cn;
 import net.fortuna.ical4j.model.property.Attendee;
 import net.fortuna.ical4j.model.property.Organizer;
@@ -18,6 +17,7 @@ import net.fortuna.ical4j.model.property.Uid;
 import net.fortuna.ical4j.model.property.XProperty;
 import net.fortuna.ical4j.validate.ValidationException;
 import org.apache.commons.codec.digest.DigestUtils;
+import org.slf4j.Logger;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.core.io.ByteArrayResource;
 import org.springframework.stereotype.Service;
@@ -27,17 +27,20 @@ import org.synyx.urlaubsverwaltung.person.Person;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.net.URI;
-import java.time.LocalDate;
-import java.time.ZoneId;
+import java.text.ParseException;
 import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.List;
 import java.util.Optional;
 
+import static java.lang.invoke.MethodHandles.lookup;
+import static java.util.Date.from;
 import static net.fortuna.ical4j.model.parameter.Role.REQ_PARTICIPANT;
+import static net.fortuna.ical4j.model.property.CalScale.GREGORIAN;
+import static net.fortuna.ical4j.model.property.Method.CANCEL;
 import static net.fortuna.ical4j.model.property.Transp.VALUE_TRANSPARENT;
-import static net.fortuna.ical4j.model.property.immutable.ImmutableCalScale.GREGORIAN;
-import static net.fortuna.ical4j.model.property.immutable.ImmutableMethod.CANCEL;
-import static net.fortuna.ical4j.model.property.immutable.ImmutableVersion.VERSION_2_0;
+import static net.fortuna.ical4j.model.property.Version.VERSION_2_0;
+import static org.slf4j.LoggerFactory.getLogger;
 import static org.synyx.urlaubsverwaltung.calendar.ICalType.CANCELLED;
 import static org.synyx.urlaubsverwaltung.calendar.ICalType.PUBLISHED;
 
@@ -45,9 +48,11 @@ import static org.synyx.urlaubsverwaltung.calendar.ICalType.PUBLISHED;
 @Service
 public class ICalService {
 
+    private static final Logger LOG = getLogger(lookup().lookupClass());
+
     private final CalendarProperties calendarProperties;
 
-    private static final ZoneId UTC = ZoneId.of("Etc/UTC");
+    private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
 
     @Autowired
     ICalService(CalendarProperties calendarProperties) {
@@ -66,8 +71,8 @@ public class ICalService {
 
     private Calendar generateCalendar(String title, List<Absence> absences, Person recipient) {
         final Calendar calendar = prepareCalendar(absences, PUBLISHED, recipient);
-        calendar.add(new XProperty("X-WR-CALNAME", title));
-        calendar.add(new RefreshInterval(new ParameterList(), calendarProperties.getRefreshInterval()));
+        calendar.getProperties().add(new XProperty("X-WR-CALNAME", title));
+        calendar.getProperties().add(new RefreshInterval(new ParameterList(), calendarProperties.getRefreshInterval()));
         return calendar;
     }
 
@@ -77,85 +82,82 @@ public class ICalService {
 
     private Calendar prepareCalendar(List<Absence> absences, ICalType method, Person recipient) {
         final Calendar calendar = new Calendar();
-        calendar.add(VERSION_2_0);
-        calendar.add(new ProdId("-//Urlaubsverwaltung//iCal4j 1.0//DE"));
-        calendar.add(GREGORIAN);
-        calendar.add(new XProperty("X-MICROSOFT-CALSCALE", GREGORIAN.getValue()));
+        calendar.getProperties().add(VERSION_2_0);
+        calendar.getProperties().add(new ProdId("-//Urlaubsverwaltung//iCal4j 1.0//DE"));
+        calendar.getProperties().add(GREGORIAN);
+        calendar.getProperties().add(new XProperty("X-MICROSOFT-CALSCALE", GREGORIAN.getValue()));
 
         if (method == CANCELLED) {
-            calendar.add(CANCEL);
+            calendar.getProperties().add(CANCEL);
         }
 
-        calendar.add(toVTimeZone());
-
         absences.stream()
             .map(absence -> this.toVEvent(absence, method, absence.getPerson().equals(recipient)))
             .filter(Optional::isPresent)
             .map(Optional::get)
-            .forEach(calendar::add);
+            .forEach(event -> calendar.getComponents().add(event));
 
         return calendar;
     }
 
-    private static VTimeZone toVTimeZone() {
-        final TimeZoneRegistry registry = TimeZoneRegistryFactory.getInstance().createRegistry();
-        return registry.getTimeZone(UTC.getId()).getVTimeZone();
-    }
-
     private Optional<VEvent> toVEvent(Absence absence, ICalType method, boolean isOwn) {
 
-        final ZonedDateTime startDateTimeInUTC = absence.getStartDate().withZoneSameInstant(UTC);
-        final ZonedDateTime endDateTimeInUTC = absence.getEndDate().withZoneSameInstant(UTC);
+        final ZonedDateTime startDateTime = absence.getStartDate();
+        final ZonedDateTime endDateTime = absence.getEndDate();
 
         final VEvent event;
         if (absence.isAllDay()) {
-            event = generateAllDayEvent(absence.getEventSubject(), startDateTimeInUTC, endDateTimeInUTC);
+            try {
+                final Date startDate = new Date(startDateTime.format(formatter));
+                if (isSameDay(startDateTime, endDateTime)) {
+                    event = new VEvent(startDate, absence.getEventSubject());
+                } else {
+                    final Date endDate = new Date(endDateTime.format(formatter));
+                    event = new VEvent(new Date(startDate), new Date(endDate), absence.getEventSubject());
+                }
+            } catch (ParseException e) {
+                LOG.warn("Could not generate all day ical event for absence {}", absence, e);
+                return Optional.empty();
+            }
+
+            event.getProperties().add(new XProperty("X-MICROSOFT-CDO-ALLDAYEVENT", "TRUE"));
         } else {
-            event = new VEvent(startDateTimeInUTC, endDateTimeInUTC, absence.getEventSubject());
+            final DateTime start = new DateTime(from(startDateTime.toInstant()));
+            start.setUtc(true);
+            final DateTime end = new DateTime(from(endDateTime.toInstant()));
+            end.setUtc(true);
+
+            event = new VEvent(start, end, absence.getEventSubject());
         }
 
-        event.add(new Uid(generateUid(absence)));
+        event.getProperties().add(new Uid(generateUid(absence)));
         if (absence.getPerson().getEmail() != null) {
-            event.add(generateAttendee(absence));
+            event.getProperties().add(generateAttendee(absence));
         }
 
         if (absence.isHolidayReplacement() || !isOwn) {
-            event.add(new Transp(VALUE_TRANSPARENT));
+            event.getProperties().add(new Transp(VALUE_TRANSPARENT));
         }
 
         if (method == CANCELLED) {
-            event.add(new Sequence(1));
+            event.getProperties().add(new Sequence(1));
         }
 
-        event.add(new Organizer(URI.create("mailto:" + calendarProperties.getOrganizer())));
+        event.getProperties().add(new Organizer(URI.create("mailto:" + calendarProperties.getOrganizer())));
 
         return Optional.of(event);
     }
 
-    private VEvent generateAllDayEvent(String eventSubject, ZonedDateTime startDateTime, ZonedDateTime endDateTime) {
-        final LocalDate startDate = startDateTime.toLocalDate();
-        final LocalDate endDate = endDateTime.toLocalDate();
-
-        final VEvent event;
-        if (isSameDay(startDate, endDate)) {
-            event = new VEvent(startDate, eventSubject);
-        } else {
-            event = new VEvent(startDate, endDate, eventSubject);
-        }
-        event.add(new XProperty("X-MICROSOFT-CDO-ALLDAYEVENT", "TRUE"));
-        return event;
-    }
-
     private Attendee generateAttendee(Absence absence) {
         final Attendee attendee = new Attendee(URI.create("mailto:" + absence.getPerson().getEmail()));
-        attendee.add(REQ_PARTICIPANT);
-        attendee.add(new Cn(absence.getPerson().getNiceName()));
+        attendee.getParameters().add(REQ_PARTICIPANT);
+        attendee.getParameters().add(new Cn(absence.getPerson().getNiceName()));
 
         return attendee;
     }
 
-    private boolean isSameDay(LocalDate startDate, LocalDate endDate) {
-        return startDate.isEqual(endDate.minusDays(1));
+    private boolean isSameDay(ZonedDateTime startDateTime, ZonedDateTime endDate) {
+        return startDateTime.toLocalDate().isEqual(endDate.toLocalDate().minusDays(1));
     }
 
     private String generateUid(Absence absence) {


Test patch:
diff --git a/src/test/java/org/synyx/urlaubsverwaltung/calendar/ICalServiceTest.java b/src/test/java/org/synyx/urlaubsverwaltung/calendar/ICalServiceTest.java
index 22814c9b9..99d77d0fd 100644
--- a/src/test/java/org/synyx/urlaubsverwaltung/calendar/ICalServiceTest.java
+++ b/src/test/java/org/synyx/urlaubsverwaltung/calendar/ICalServiceTest.java
@@ -51,19 +51,6 @@ class ICalServiceTest {
                 X-MICROSOFT-CALSCALE:GREGORIAN
                 X-WR-CALNAME:Abwesenheitskalender
                 REFRESH-INTERVAL:P1D
-                BEGIN:VTIMEZONE
-                TZID:Etc/UTC
-                LAST-MODIFIED:20240422T053450Z
-                TZURL:https://www.tzurl.org/zoneinfo/Etc/UTC
-                X-LIC-LOCATION:Etc/UTC
-                X-PROLEPTIC-TZNAME:UTC
-                BEGIN:STANDARD
-                TZNAME:UTC
-                TZOFFSETFROM:+0000
-                TZOFFSETTO:+0000
-                DTSTART:16010101T000000
-                END:STANDARD
-                END:VTIMEZONE
                 END:VCALENDAR
                 """);
     }
@@ -86,19 +73,6 @@ class ICalServiceTest {
                 X-MICROSOFT-CALSCALE:GREGORIAN
                 X-WR-CALNAME:Abwesenheitskalender
                 REFRESH-INTERVAL:P1D
-                BEGIN:VTIMEZONE
-                TZID:Etc/UTC
-                LAST-MODIFIED:20240422T053450Z
-                TZURL:https://www.tzurl.org/zoneinfo/Etc/UTC
-                X-LIC-LOCATION:Etc/UTC
-                X-PROLEPTIC-TZNAME:UTC
-                BEGIN:STANDARD
-                TZNAME:UTC
-                TZOFFSETFROM:+0000
-                TZOFFSETTO:+0000
-                DTSTART:16010101T000000
-                END:STANDARD
-                END:VTIMEZONE
                 BEGIN:VEVENT
                 DTSTAMP:<removedByConversionMethod>
                 DTSTART;VALUE=DATE:20190326
@@ -131,23 +105,10 @@ class ICalServiceTest {
                 X-MICROSOFT-CALSCALE:GREGORIAN
                 X-WR-CALNAME:Abwesenheitskalender
                 REFRESH-INTERVAL:P1D
-                BEGIN:VTIMEZONE
-                TZID:Etc/UTC
-                LAST-MODIFIED:20240422T053450Z
-                TZURL:https://www.tzurl.org/zoneinfo/Etc/UTC
-                X-LIC-LOCATION:Etc/UTC
-                X-PROLEPTIC-TZNAME:UTC
-                BEGIN:STANDARD
-                TZNAME:UTC
-                TZOFFSETFROM:+0000
-                TZOFFSETTO:+0000
-                DTSTART:16010101T000000
-                END:STANDARD
-                END:VTIMEZONE
                 BEGIN:VEVENT
                 DTSTAMP:<removedByConversionMethod>
-                DTSTART;TZID=Etc/UTC:20190426T080000
-                DTEND;TZID=Etc/UTC:20190426T120000
+                DTSTART:20190426T080000Z
+                DTEND:20190426T120000Z
                 SUMMARY:Marlene Muster abwesend
                 UID:45BCD3F64AD76CEF9040DF93047C41B3
                 ATTENDEE;ROLE=REQ-PARTICIPANT;CN=Marlene Muster:mailto:muster@example.org
@@ -177,19 +138,6 @@ class ICalServiceTest {
                 X-MICROSOFT-CALSCALE:GREGORIAN
                 X-WR-CALNAME:Abwesenheitskalender
                 REFRESH-INTERVAL:P1D
-                BEGIN:VTIMEZONE
-                TZID:Etc/UTC
-                LAST-MODIFIED:20240422T053450Z
-                TZURL:https://www.tzurl.org/zoneinfo/Etc/UTC
-                X-LIC-LOCATION:Etc/UTC
-                X-PROLEPTIC-TZNAME:UTC
-                BEGIN:STANDARD
-                TZNAME:UTC
-                TZOFFSETFROM:+0000
-                TZOFFSETTO:+0000
-                DTSTART:16010101T000000
-                END:STANDARD
-                END:VTIMEZONE
                 BEGIN:VEVENT
                 DTSTAMP:<removedByConversionMethod>
                 DTSTART;VALUE=DATE:20190326
@@ -223,23 +171,10 @@ class ICalServiceTest {
                 X-MICROSOFT-CALSCALE:GREGORIAN
                 X-WR-CALNAME:Abwesenheitskalender
                 REFRESH-INTERVAL:P1D
-                BEGIN:VTIMEZONE
-                TZID:Etc/UTC
-                LAST-MODIFIED:20240422T053450Z
-                TZURL:https://www.tzurl.org/zoneinfo/Etc/UTC
-                X-LIC-LOCATION:Etc/UTC
-                X-PROLEPTIC-TZNAME:UTC
-                BEGIN:STANDARD
-                TZNAME:UTC
-                TZOFFSETFROM:+0000
-                TZOFFSETTO:+0000
-                DTSTART:16010101T000000
-                END:STANDARD
-                END:VTIMEZONE
                 BEGIN:VEVENT
                 DTSTAMP:<removedByConversionMethod>
-                DTSTART;TZID=Etc/UTC:20190526T120000
-                DTEND;TZID=Etc/UTC:20190526T160000
+                DTSTART:20190526T120000Z
+                DTEND:20190526T160000Z
                 SUMMARY:Marlene Muster abwesend
                 UID:BB2267885BD8DF263E88D3062853E8A7
                 ATTENDEE;ROLE=REQ-PARTICIPANT;CN=Marlene Muster:mailto:muster@example.org
@@ -271,23 +206,10 @@ class ICalServiceTest {
                 X-MICROSOFT-CALSCALE:GREGORIAN
                 X-WR-CALNAME:Abwesenheitskalender
                 REFRESH-INTERVAL:P1D
-                BEGIN:VTIMEZONE
-                TZID:Etc/UTC
-                LAST-MODIFIED:20240422T053450Z
-                TZURL:https://www.tzurl.org/zoneinfo/Etc/UTC
-                X-LIC-LOCATION:Etc/UTC
-                X-PROLEPTIC-TZNAME:UTC
-                BEGIN:STANDARD
-                TZNAME:UTC
-                TZOFFSETFROM:+0000
-                TZOFFSETTO:+0000
-                DTSTART:16010101T000000
-                END:STANDARD
-                END:VTIMEZONE
                 BEGIN:VEVENT
                 DTSTAMP:<removedByConversionMethod>
-                DTSTART;TZID=Etc/UTC:20190526T120000
-                DTEND;TZID=Etc/UTC:20190526T160000
+                DTSTART:20190526T120000Z
+                DTEND:20190526T160000Z
                 SUMMARY:Marlene Muster abwesend
                 UID:BB2267885BD8DF263E88D3062853E8A7
                 ATTENDEE;ROLE=REQ-PARTICIPANT;CN=Marlene Muster:mailto:muster@example.org
@@ -319,23 +241,10 @@ class ICalServiceTest {
                 CALSCALE:GREGORIAN
                 X-MICROSOFT-CALSCALE:GREGORIAN
                 METHOD:CANCEL
-                BEGIN:VTIMEZONE
-                TZID:Etc/UTC
-                LAST-MODIFIED:20240422T053450Z
-                TZURL:https://www.tzurl.org/zoneinfo/Etc/UTC
-                X-LIC-LOCATION:Etc/UTC
-                X-PROLEPTIC-TZNAME:UTC
-                BEGIN:STANDARD
-                TZNAME:UTC
-                TZOFFSETFROM:+0000
-                TZOFFSETTO:+0000
-                DTSTART:16010101T000000
-                END:STANDARD
-                END:VTIMEZONE
                 BEGIN:VEVENT
                 DTSTAMP:<removedByConversionMethod>
-                DTSTART;TZID=Etc/UTC:20190526T120000
-                DTEND;TZID=Etc/UTC:20190526T160000
+                DTSTART:20190526T120000Z
+                DTEND:20190526T160000Z
                 SUMMARY:Marlene Muster abwesend
                 UID:BB2267885BD8DF263E88D3062853E8A7
                 ATTENDEE;ROLE=REQ-PARTICIPANT;CN=Marlene Muster:mailto:muster@example.org
@@ -366,23 +275,10 @@ class ICalServiceTest {
                 PRODID:-//Urlaubsverwaltung//iCal4j 1.0//DE
                 CALSCALE:GREGORIAN
                 X-MICROSOFT-CALSCALE:GREGORIAN
-                BEGIN:VTIMEZONE
-                TZID:Etc/UTC
-                LAST-MODIFIED:20240422T053450Z
-                TZURL:https://www.tzurl.org/zoneinfo/Etc/UTC
-                X-LIC-LOCATION:Etc/UTC
-                X-PROLEPTIC-TZNAME:UTC
-                BEGIN:STANDARD
-                TZNAME:UTC
-                TZOFFSETFROM:+0000
-                TZOFFSETTO:+0000
-                DTSTART:16010101T000000
-                END:STANDARD
-                END:VTIMEZONE
                 BEGIN:VEVENT
                 DTSTAMP:<removedByConversionMethod>
-                DTSTART;TZID=Etc/UTC:20190526T120000
-                DTEND;TZID=Etc/UTC:20190526T160000
+                DTSTART:20190526T120000Z
+                DTEND:20190526T160000Z
                 SUMMARY:Marlene Muster abwesend
                 UID:BB2267885BD8DF263E88D3062853E8A7
                 ATTENDEE;ROLE=REQ-PARTICIPANT;CN=Marlene Muster:mailto:muster@example.org
@@ -416,19 +312,6 @@ class ICalServiceTest {
                 PRODID:-//Urlaubsverwaltung//iCal4j 1.0//DE
                 CALSCALE:GREGORIAN
                 X-MICROSOFT-CALSCALE:GREGORIAN
-                BEGIN:VTIMEZONE
-                TZID:Etc/UTC
-                LAST-MODIFIED:20240422T053450Z
-                TZURL:https://www.tzurl.org/zoneinfo/Etc/UTC
-                X-LIC-LOCATION:Etc/UTC
-                X-PROLEPTIC-TZNAME:UTC
-                BEGIN:STANDARD
-                TZNAME:UTC
-                TZOFFSETFROM:+0000
-                TZOFFSETTO:+0000
-                DTSTART:16010101T000000
-                END:STANDARD
-                END:VTIMEZONE
                 BEGIN:VEVENT
                 DTSTAMP:<removedByConversionMethod>
                 DTSTART;VALUE=DATE:20190526
@@ -467,19 +350,6 @@ class ICalServiceTest {
                 PRODID:-//Urlaubsverwaltung//iCal4j 1.0//DE
                 CALSCALE:GREGORIAN
                 X-MICROSOFT-CALSCALE:GREGORIAN
-                BEGIN:VTIMEZONE
-                TZID:Etc/UTC
-                LAST-MODIFIED:20240422T053450Z
-                TZURL:https://www.tzurl.org/zoneinfo/Etc/UTC
-                X-LIC-LOCATION:Etc/UTC
-                X-PROLEPTIC-TZNAME:UTC
-                BEGIN:STANDARD
-                TZNAME:UTC
-                TZOFFSETFROM:+0000
-                TZOFFSETTO:+0000
-                DTSTART:16010101T000000
-                END:STANDARD
-                END:VTIMEZONE
                 BEGIN:VEVENT
                 DTSTAMP:<removedByConversionMethod>
                 DTSTART;VALUE=DATE:20190526
@@ -512,19 +382,6 @@ class ICalServiceTest {
                 PRODID:-//Urlaubsverwaltung//iCal4j 1.0//DE
                 CALSCALE:GREGORIAN
                 X-MICROSOFT-CALSCALE:GREGORIAN
-                BEGIN:VTIMEZONE
-                TZID:Etc/UTC
-                LAST-MODIFIED:20240422T053450Z
-                TZURL:https://www.tzurl.org/zoneinfo/Etc/UTC
-                X-LIC-LOCATION:Etc/UTC
-                X-PROLEPTIC-TZNAME:UTC
-                BEGIN:STANDARD
-                TZNAME:UTC
-                TZOFFSETFROM:+0000
-                TZOFFSETTO:+0000
-                DTSTART:16010101T000000
-                END:STANDARD
-                END:VTIMEZONE
                 BEGIN:VEVENT
                 DTSTAMP:<removedByConversionMethod>
                 DTSTART;VALUE=DATE:20190526