diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt b/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt index ff80ed45..dcd7ffd4 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt @@ -12,33 +12,38 @@ import at.bitfire.synctools.exception.InvalidICalendarException import at.bitfire.synctools.icalendar.ICalendarParser import at.bitfire.synctools.icalendar.propertyListOf import at.bitfire.synctools.icalendar.validation.ICalPreprocessor +import net.fortuna.ical4j.data.CalendarBuilder +import net.fortuna.ical4j.data.ParserException import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.ComponentList -import net.fortuna.ical4j.model.Date +import net.fortuna.ical4j.model.Parameter import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.TemporalAdapter -import net.fortuna.ical4j.model.TemporalComparator import net.fortuna.ical4j.model.component.Daylight import net.fortuna.ical4j.model.component.Observance import net.fortuna.ical4j.model.component.Standard import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.component.VTimeZone import net.fortuna.ical4j.model.parameter.Related +import net.fortuna.ical4j.model.property.Color import net.fortuna.ical4j.model.property.DateProperty import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.ProdId import net.fortuna.ical4j.model.property.RDate import net.fortuna.ical4j.model.property.RRule +import net.fortuna.ical4j.model.property.Trigger import net.fortuna.ical4j.validate.ValidationException import java.io.Reader +import java.io.StringReader +import java.time.Duration +import java.time.Period import java.time.ZonedDateTime import java.time.temporal.Temporal -import java.time.temporal.TemporalAccessor -import java.time.temporal.TemporalAdjuster import java.util.LinkedList import java.util.UUID import java.util.logging.Level import java.util.logging.Logger +import kotlin.jvm.optionals.getOrNull open class ICalendar { @@ -101,17 +106,16 @@ open class ICalendar { // fill calendar properties properties?.let { - TODO("ical4j 4.x") - /*calendar.getProperty(CALENDAR_NAME)?.let { calName -> + calendar.getProperty(CALENDAR_NAME).getOrNull()?.let { calName -> properties[CALENDAR_NAME] = calName.value } - calendar.getProperty(Color.PROPERTY_NAME)?.let { calColor -> + calendar.getProperty(Color.PROPERTY_NAME).getOrNull()?.let { calColor -> properties[Color.PROPERTY_NAME] = calColor.value } - calendar.getProperty(CALENDAR_COLOR)?.let { calColor -> + calendar.getProperty(CALENDAR_COLOR).getOrNull()?.let { calColor -> properties[CALENDAR_COLOR] = calColor.value - }*/ + } } return calendar @@ -223,15 +227,14 @@ open class ICalendar { * @return time zone id (TZID) if VTIMEZONE contains a TZID, null otherwise */ fun timezoneDefToTzId(timezoneDef: String): String? { - TODO("ical4j 4.x") - /*try { + try { val builder = CalendarBuilder() val cal = builder.build(StringReader(timezoneDef)) - val timezone = cal.getComponent(VTimeZone.VTIMEZONE) as VTimeZone? + val timezone = cal.getComponent(VTimeZone.VTIMEZONE).getOrNull() timezone?.timeZoneId?.let { return it.value } } catch (e: ParserException) { logger.log(Level.SEVERE, "Can't understand time zone definition", e) - }*/ + } return null } @@ -261,7 +264,7 @@ open class ICalendar { // misc. iCalendar helpers /** - * Calculates the minutes before/after an event/task a given alarm occurs. + * Calculates the minutes before/after an event/task to know when a given alarm occurs. * * @param alarm the alarm to calculate the minutes from * @param refStart reference `DTSTART` from the calendar component @@ -286,34 +289,34 @@ open class ICalendar { refDuration: net.fortuna.ical4j.model.property.Duration?, allowRelEnd: Boolean ): Pair? { - val trigger = alarm.trigger ?: return null + val trigger = alarm.getProperty(Property.TRIGGER).getOrNull() ?: return null - TODO("ical4j 4.x") // Note: big method – maybe split? - /*val minutes: Int // minutes before/after the event - var related = trigger.getParameter(Parameter.RELATED) ?: Related.START + val minutes: Int // minutes before/after the event + var related: Related = trigger.getParameter(Parameter.RELATED).getOrNull() ?: Related.START - // event/task start time - val start: java.util.Date? = refStart?.date - var end: java.util.Date? = refEnd?.date + // event/task start/end time + val start: Temporal? = refStart?.date + var end: Temporal? = refEnd?.date // event/task end time - if (end == null && start != null) { - val duration = refDuration?.duration - if (duration != null) - end = java.util.Date.from(start.toInstant() + duration) - } + if (end == null && start != null) + end = when (val refDur = refDuration?.duration) { + is Duration -> start + refDur + is Period -> start + Duration.between(start, start + refDur) + else -> null + } // event/task duration val duration: Duration? = if (start != null && end != null) - Duration.between(start.toInstant(), end.toInstant()) + Duration.between(start, end) else null val triggerDur = trigger.duration - val triggerTime = trigger.dateTime + val triggerTime = trigger.date if (triggerDur != null) { // TRIGGER value is a DURATION. Important: @@ -323,9 +326,11 @@ open class ICalendar { var millisBefore = when (triggerDur) { is Duration -> -triggerDur.toMillis() - is Period -> // TODO: Take time zones into account (will probably be possible with ical4j 4.x). + is Period -> { + // TODO: Take time zones into account (will probably be possible with ical4j 4.x). // For instance, an alarm one day before the DST change should be 23/25 hours before the event. - -triggerDur.days.toLong()*24*3600000 // months and years are not used in DURATION values; weeks are calculated to days + -Duration.ofDays(triggerDur.days.toLong()).toMillis() // months and years are not used in DURATION values; weeks are calculated to days + } else -> throw AssertionError("triggerDur must be Duration or Period") } @@ -343,14 +348,14 @@ open class ICalendar { } else if (triggerTime != null && start != null) { // TRIGGER value is a DATE-TIME, calculate minutes from start time related = Related.START - minutes = Duration.between(triggerTime.toInstant(), start.toInstant()).toMinutes().toInt() + minutes = Duration.between(triggerTime, start).toMinutes().toInt() } else { logger.log(Level.WARNING, "VALARM TRIGGER type is not DURATION or DATE-TIME (requires event DTSTART for Android), ignoring alarm", alarm) return null } - return Pair(related, minutes)*/ + return Pair(related, minutes) } } diff --git a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessor.kt index b7ec7ade..d030c782 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessor.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessor.kt @@ -10,6 +10,7 @@ import androidx.annotation.VisibleForTesting import com.google.common.io.CharSource import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.component.CalendarComponent import net.fortuna.ical4j.transform.compliance.DateListPropertyRule import net.fortuna.ical4j.transform.compliance.DatePropertyRule import net.fortuna.ical4j.transform.compliance.Rfc5545PropertyRule diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt index f6c40a77..6ea598bf 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt @@ -8,8 +8,8 @@ package at.bitfire.ical4android import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.model.Component -import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.Property.TRIGGER import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.VAlarm @@ -19,16 +19,19 @@ import net.fortuna.ical4j.model.property.Color import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.Due +import net.fortuna.ical4j.model.property.Duration +import net.fortuna.ical4j.model.property.Trigger import net.fortuna.ical4j.util.TimeZones import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull -import org.junit.Ignore import org.junit.Test import java.io.StringReader import java.time.LocalDateTime +import java.time.Period import java.time.ZonedDateTime -import java.util.Date +import java.time.temporal.Temporal +import kotlin.jvm.optionals.getOrNull class ICalendarTest { @@ -47,7 +50,7 @@ class ICalendarTest { private val vtzMogadishu = readTimeZone("Mogadishu.ics") // current time stamp - private val currentTime = Date().time + private val currentTime = ZonedDateTime.now() private fun readTimeZone(fileName: String): VTimeZone { @@ -58,7 +61,7 @@ class ICalendarTest { } } - @Ignore("ical4j 4.x") + @Test fun testFromReader_calendarProperties() { val calendar = ICalendar.fromReader( @@ -73,13 +76,11 @@ class ICalendarTest { "END:VCALENDAR" ) ) - TODO("ical4j 4.x") - /*assertEquals("Some Calendar", calendar.getProperty(ICalendar.CALENDAR_NAME).value) - assertEquals("darkred", calendar.getProperty(Color.PROPERTY_NAME).value) - assertEquals("#123456", calendar.getProperty(ICalendar.CALENDAR_COLOR).value)*/ + assertEquals("Some Calendar", calendar.getProperty(ICalendar.CALENDAR_NAME).getOrNull()?.value) + assertEquals("darkred", calendar.getProperty(Color.PROPERTY_NAME).getOrNull()?.value) + assertEquals("#123456", calendar.getProperty(ICalendar.CALENDAR_COLOR).getOrNull()?.value) } - @Ignore("ical4j 4.x") @Test fun testFromReader_invalidProperty() { // The GEO property is invalid and should be ignored. @@ -185,7 +186,6 @@ class ICalendarTest { } - @Ignore("ical4j 4.x") @Test fun testTimezoneDefToTzId_Valid() { assertEquals( @@ -216,7 +216,6 @@ class ICalendarTest { ) } - @Ignore("ical4j 4.x") @Test fun testTimezoneDefToTzId_Invalid() { // invalid time zone @@ -234,12 +233,12 @@ class ICalendarTest { } - /*@Test + @Test fun testVAlarmToMin_TriggerDuration_Negative() { // TRIGGER;REL=START:-P1DT1H1M29S val (ref, min) = ICalendar.vAlarmToMin( - VAlarm(Duration.parse("-P1DT1H1M29S")), - DtStart(), null, null, false + VAlarm(Duration("-P1DT1H1M29S").duration), + DtStart(), null, null, false )!! assertEquals(Related.START, ref) assertEquals(60 * 24 + 60 + 1, min) @@ -249,8 +248,8 @@ class ICalendarTest { fun testVAlarmToMin_TriggerDuration_OnlySeconds() { // TRIGGER;REL=START:-PT3600S val (ref, min) = ICalendar.vAlarmToMin( - VAlarm(Duration.parse("-PT3600S")), - DtStart(), null, null, false + VAlarm(Duration("-PT3600S").duration), + DtStart(), null, null, false )!! assertEquals(Related.START, ref) assertEquals(60, min) @@ -260,8 +259,8 @@ class ICalendarTest { fun testVAlarmToMin_TriggerDuration_Positive() { // TRIGGER;REL=START:P1DT1H1M30S (alarm *after* start) val (ref, min) = ICalendar.vAlarmToMin( - VAlarm(Duration.parse("P1DT1H1M30S")), - DtStart(), null, null, false + VAlarm(Duration("P1DT1H1M30S").duration), + DtStart(), null, null, false )!! assertEquals(Related.START, ref) assertEquals(-(60 * 24 + 60 + 1), min) @@ -270,9 +269,9 @@ class ICalendarTest { @Test fun testVAlarmToMin_TriggerDuration_RelEndAllowed() { // TRIGGER;REL=END:-P1DT1H1M30S (caller accepts Related.END) - val alarm = VAlarm(Duration.parse("-P1DT1H1M30S")) - alarm.trigger.parameters.add(Related.END) - val (ref, min) = ICalendar.vAlarmToMin(alarm, DtStart(), null, null, true)!! + val alarm = VAlarm(Duration("-P1DT1H1M30S").duration) + alarm.getProperty(TRIGGER).getOrNull()?.add(Related.END) + val (ref, min) = ICalendar.vAlarmToMin(alarm, DtStart(), null, null, true)!! assertEquals(Related.END, ref) assertEquals(60 * 24 + 60 + 1, min) } @@ -280,12 +279,12 @@ class ICalendarTest { @Test fun testVAlarmToMin_TriggerDuration_RelEndNotAllowed() { // event with TRIGGER;REL=END:-PT30S (caller doesn't accept Related.END) - val alarm = VAlarm(Duration.parse("-PT65S")) - alarm.trigger.parameters.add(Related.END) + val alarm = VAlarm(Duration("-PT65S").duration) + alarm.getProperty(TRIGGER).getOrNull()?.add(Related.END) val (ref, min) = ICalendar.vAlarmToMin( alarm, - DtStart(DateTime(currentTime)), - DtEnd(DateTime(currentTime + 180 * 1000)), // 180 sec later + DtStart(currentTime), + DtEnd(currentTime.plusSeconds(180)), // 180 sec later null, false )!! @@ -297,40 +296,40 @@ class ICalendarTest { @Test fun testVAlarmToMin_TriggerDuration_RelEndNotAllowed_NoDtStart() { // event with TRIGGER;REL=END:-PT30S (caller doesn't accept Related.END) - val alarm = VAlarm(Duration.parse("-PT65S")) - alarm.trigger.parameters.add(Related.END) - assertNull(ICalendar.vAlarmToMin(alarm, DtStart(), DtEnd(DateTime(currentTime)), null, false)) + val alarm = VAlarm(Duration("-PT65S").duration) + alarm.getProperty(TRIGGER).getOrNull()?.add(Related.END) + assertNull(ICalendar.vAlarmToMin(alarm, DtStart(), DtEnd(currentTime), null, false)) } @Test fun testVAlarmToMin_TriggerDuration_RelEndNotAllowed_NoDuration() { // event with TRIGGER;REL=END:-PT30S (caller doesn't accept Related.END) - val alarm = VAlarm(Duration.parse("-PT65S")) - alarm.trigger.parameters.add(Related.END) - assertNull(ICalendar.vAlarmToMin(alarm, DtStart(DateTime(currentTime)), null, null, false)) + val alarm = VAlarm(Duration("-PT65S").duration) + alarm.getProperty(TRIGGER).getOrNull()?.add(Related.END) + assertNull(ICalendar.vAlarmToMin(alarm, DtStart(currentTime), null, null, false)) } @Test fun testVAlarmToMin_TriggerDuration_RelEndNotAllowed_AfterEnd() { // task with TRIGGER;REL=END:-P1DT1H1M30S (caller doesn't accept Related.END; alarm *after* end) - val alarm = VAlarm(Duration.parse("P1DT1H1M30S")) - alarm.trigger.parameters.add(Related.END) + val alarm = VAlarm(Duration("P1DT1H1M30S").duration) + alarm.getProperty(TRIGGER).getOrNull()?.add(Related.END) val (ref, min) = ICalendar.vAlarmToMin( alarm, - DtStart(DateTime(currentTime)), - Due(DateTime(currentTime + 90 * 1000)), // 90 sec (should be rounded down to 1 min) later + DtStart(currentTime), + Due(currentTime.plusSeconds(90)), // 90 sec (should be rounded down to 1 min) later null, false )!! assertEquals(Related.START, ref) - assertEquals(-(60 * 24 + 60 + 1 + 1) *//* duration of event: *//* - 1, min) + assertEquals(-(60 * 24 + 60 + 1 + 1) /* duration of event: */ - 1, min) } @Test fun testVAlarm_TriggerPeriod() { val (ref, min) = ICalendar.vAlarmToMin( VAlarm(Period.parse("-P1W1D")), - DtStart(net.fortuna.ical4j.model.Date(currentTime)), null, null, + DtStart(currentTime), null, null, false )!! assertEquals(Related.START, ref) @@ -340,12 +339,12 @@ class ICalendarTest { @Test fun testVAlarm_TriggerAbsoluteValue() { // TRIGGER;VALUE=DATE-TIME: - val alarm = VAlarm(DateTime(currentTime - 89 * 1000)) // 89 sec (should be cut off to 1 min) before event - alarm.trigger.parameters.add(Related.END) // not useful for DATE-TIME values, should be ignored - val (ref, min) = ICalendar.vAlarmToMin(alarm, DtStart(DateTime(currentTime)), null, null, false)!! + val alarm = VAlarm(currentTime.minusSeconds(89).toInstant()) // 89 sec (should be cut off to 1 min) before event + alarm.getProperty(TRIGGER).getOrNull()?.add(Related.END) // not useful for DATE-TIME values, should be ignored + val (ref, min) = ICalendar.vAlarmToMin(alarm, DtStart(currentTime), null, null, false)!! assertEquals(Related.START, ref) assertEquals(1, min) - }*/ + } // TODO Note: can we use the following now when we have ical4j 4.x?