From dc267bf6b8a7aaefbc4701d3ca2bab264f091f1e Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 9 Jul 2025 18:03:20 +0200 Subject: [PATCH 1/4] Extract EventWriter --- .../at/bitfire/ical4android/EventTest.kt | 89 --------- .../kotlin/at/bitfire/ical4android/Event.kt | 131 -------------- .../at/bitfire/ical4android/EventWriter.kt | 169 ++++++++++++++++++ .../bitfire/ical4android/EventWriterTest.kt | 111 ++++++++++++ 4 files changed, 280 insertions(+), 220 deletions(-) create mode 100644 lib/src/main/kotlin/at/bitfire/ical4android/EventWriter.kt create mode 100644 lib/src/test/kotlin/at/bitfire/ical4android/EventWriterTest.kt diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt index 73b41b60..a1a89844 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt @@ -6,32 +6,20 @@ package at.bitfire.ical4android -import at.bitfire.ical4android.impl.testProdId import at.bitfire.ical4android.util.DateUtils import at.bitfire.synctools.icalendar.Css3Color -import net.fortuna.ical4j.model.Date -import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.model.Parameter import net.fortuna.ical4j.model.TimeZoneRegistryFactory -import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.parameter.Email -import net.fortuna.ical4j.model.property.Attendee -import net.fortuna.ical4j.model.property.DtEnd -import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.Organizer -import net.fortuna.ical4j.model.property.RRule -import net.fortuna.ical4j.model.property.RecurrenceId import net.fortuna.ical4j.util.TimeZones import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test -import java.io.ByteArrayOutputStream import java.io.FileNotFoundException import java.io.InputStreamReader import java.nio.charset.Charset -import java.time.Duration class EventTest { @@ -74,21 +62,6 @@ class EventTest { assertEquals(1522738800000L, dtStart.date.time) } - @Test - fun testGenerateEtcUTC() { - val e = Event() - e.uid = "etc-utc-test@example.com" - e.dtStart = DtStart("20200926T080000", tzUTC) - e.dtEnd = DtEnd("20200926T100000", tzUTC) - e.alarms += VAlarm(Duration.ofMinutes(-30)) - e.attendees += Attendee("mailto:test@example.com") - val baos = ByteArrayOutputStream() - e.write(baos, testProdId) - val ical = baos.toString() - - assertTrue("BEGIN:VTIMEZONE.+BEGIN:STANDARD.+END:STANDARD.+END:VTIMEZONE".toRegex(RegexOption.DOT_MATCHES_ALL).containsMatchIn(ical)) - } - @Test fun testGrouping() { val events = parseCalendar("multiple.ics") @@ -127,44 +100,6 @@ class EventTest { assertEquals("Unknown Value", unknown.value) } - @Test - fun testRecurringWriteFullDayException() { - val event = Event().apply { - uid = "test1" - dtStart = DtStart("20190117T083000", tzBerlin) - summary = "Main event" - rRules += RRule("FREQ=DAILY;COUNT=5") - exceptions += arrayOf( - Event().apply { - uid = "test2" - recurrenceId = RecurrenceId(DateTime("20190118T073000", tzLondon)) - summary = "Normal exception" - }, - Event().apply { - uid = "test3" - recurrenceId = RecurrenceId(Date("20190223")) - summary = "Full-day exception" - } - ) - } - val baos = ByteArrayOutputStream() - event.write(baos, testProdId) - val iCal = baos.toString() - assertTrue(iCal.contains("UID:test1\r\n")) - assertTrue(iCal.contains("DTSTART;TZID=Europe/Berlin:20190117T083000\r\n")) - - // first RECURRENCE-ID has been rewritten - // - to main event's UID - // - to time zone Europe/Berlin (with one hour time difference) - assertTrue(iCal.contains("UID:test1\r\n" + - "RECURRENCE-ID;TZID=Europe/Berlin:20190118T083000\r\n" + - "SUMMARY:Normal exception\r\n" + - "END:VEVENT")) - - // no RECURRENCE-ID;VALUE=DATE:20190223 - assertFalse(iCal.contains(":20190223")) - } - @Test fun testRecurringWithException() { val event = parseCalendar("recurring-with-exception1.ics").first() @@ -247,30 +182,6 @@ class EventTest { } - /* generating */ - - @Test - fun testWrite() { - val e = Event() - e.uid = "SAMPLEUID" - e.dtStart = DtStart("20190101T100000", tzBerlin) - e.alarms += VAlarm(Duration.ofHours(-1)) - - val os = ByteArrayOutputStream() - e.write(os, testProdId) - val raw = os.toString(Charsets.UTF_8.name()) - - assertTrue(raw.contains("PRODID:${testProdId.value}")) - assertTrue(raw.contains("UID:SAMPLEUID")) - assertTrue(raw.contains("DTSTART;TZID=Europe/Berlin:20190101T100000")) - assertTrue(raw.contains("DTSTAMP:")) - assertTrue(raw.contains("BEGIN:VALARM\r\n" + - "TRIGGER:-PT1H\r\n" + - "END:VALARM\r\n")) - assertTrue(raw.contains("BEGIN:VTIMEZONE")) - } - - /* internal tests */ @Test diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt index 3517c0c3..e346f2f4 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt @@ -7,19 +7,13 @@ package at.bitfire.ical4android import at.bitfire.ical4android.ICalendar.Companion.CALENDAR_NAME -import at.bitfire.ical4android.util.DateUtils.isDateTime import at.bitfire.ical4android.validation.EventValidator -import at.bitfire.synctools.exception.InvalidLocalResourceException import at.bitfire.synctools.icalendar.CalendarUidSplitter import at.bitfire.synctools.icalendar.Css3Color -import net.fortuna.ical4j.data.CalendarOutputter import net.fortuna.ical4j.data.ParserException -import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.Component import net.fortuna.ical4j.model.Parameter import net.fortuna.ical4j.model.Property -import net.fortuna.ical4j.model.TextList -import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.parameter.Email @@ -47,9 +41,7 @@ import net.fortuna.ical4j.model.property.Summary import net.fortuna.ical4j.model.property.Transp import net.fortuna.ical4j.model.property.Uid import net.fortuna.ical4j.model.property.Url -import net.fortuna.ical4j.model.property.Version import java.io.IOException -import java.io.OutputStream import java.io.Reader import java.net.URI import java.util.LinkedList @@ -100,129 +92,6 @@ data class Event( val unknownProperties: LinkedList = LinkedList() ) : ICalendar() { - /** - * Generates an iCalendar from the Event. - * - * @param os stream that the iCalendar is written to - * @param prodId `PRODID` that identifies the app - */ - fun write(os: OutputStream, prodId: ProdId) { - val ical = Calendar() - ical.properties += Version.VERSION_2_0 - ical.properties += prodId.withUserAgents(userAgents) - - val dtStart = dtStart ?: throw InvalidLocalResourceException("Won't generate event without start time") - - EventValidator.repair(this) // repair this event before creating the VEVENT - - // "main event" (without exceptions) - val components = ical.components - val mainEvent = toVEvent() - components += mainEvent - - // remember used time zones - val usedTimeZones = mutableSetOf() - dtStart.timeZone?.let(usedTimeZones::add) - dtEnd?.timeZone?.let(usedTimeZones::add) - - // recurrence exceptions - for (exception in exceptions) { - // exceptions must always have the same UID as the main event - exception.uid = uid - - val recurrenceId = exception.recurrenceId - if (recurrenceId == null) { - logger.warning("Ignoring exception without recurrenceId") - continue - } - - /* Exceptions must always have the same value type as DTSTART [RFC 5545 3.8.4.4]. - If this is not the case, we don't add the exception to the event because we're - strict in what we send (and servers may reject such a case). - */ - if (isDateTime(recurrenceId) != isDateTime(dtStart)) { - logger.warning("Ignoring exception $recurrenceId with other date type than dtStart: $dtStart") - continue - } - - // for simplicity and compatibility, rewrite date-time exceptions to the same time zone as DTSTART - if (isDateTime(recurrenceId) && recurrenceId.timeZone != dtStart.timeZone) { - logger.fine("Changing timezone of $recurrenceId to same time zone as dtStart: $dtStart") - recurrenceId.timeZone = dtStart.timeZone - } - - // create and add VEVENT for exception - val vException = exception.toVEvent() - components += vException - - // remember used time zones - exception.dtStart?.timeZone?.let(usedTimeZones::add) - exception.dtEnd?.timeZone?.let(usedTimeZones::add) - } - - // determine first dtStart (there may be exceptions with an earlier DTSTART that the main event) - val dtStarts = mutableListOf(dtStart.date) - dtStarts.addAll(exceptions.mapNotNull { it.dtStart?.date }) - val earliest = dtStarts.minOrNull() - // add VTIMEZONE components - for (tz in usedTimeZones) - ical.components += minifyVTimeZone(tz.vTimeZone, earliest) - - softValidate(ical) - CalendarOutputter(false).output(ical, os) - } - - /** - * Generates a VEvent representation of this event. - * - * @return generated VEvent - */ - private fun toVEvent(): VEvent { - val event = VEvent(/* generates DTSTAMP */) - val props = event.properties - props += Uid(uid) - - recurrenceId?.let { props += it } - sequence?.let { - if (it != 0) - props += Sequence(it) - } - - summary?.let { props += Summary(it) } - location?.let { props += Location(it) } - url?.let { props += Url(it) } - description?.let { props += Description(it) } - color?.let { props += Color(null, it.name) } - - dtStart?.let { props += it } - dtEnd?.let { props += it } - duration?.let { props += it } - - props.addAll(rRules) - props.addAll(rDates) - props.addAll(exRules) - props.addAll(exDates) - - classification?.let { props += it } - status?.let { props += it } - if (!opaque) - props += Transp.TRANSPARENT - - organizer?.let { props += it } - props.addAll(attendees) - - if (categories.isNotEmpty()) - props += Categories(TextList(categories.toTypedArray())) - props.addAll(unknownProperties) - - lastModified?.let { props += it } - - event.components.addAll(alarms) - - return event - } - - val organizerEmail: String? get() { var email: String? = null diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/EventWriter.kt b/lib/src/main/kotlin/at/bitfire/ical4android/EventWriter.kt new file mode 100644 index 00000000..3bd5683f --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/ical4android/EventWriter.kt @@ -0,0 +1,169 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.ical4android + +import at.bitfire.ical4android.ICalendar.Companion.minifyVTimeZone +import at.bitfire.ical4android.ICalendar.Companion.softValidate +import at.bitfire.ical4android.ICalendar.Companion.withUserAgents +import at.bitfire.ical4android.util.DateUtils.isDateTime +import at.bitfire.ical4android.validation.EventValidator +import at.bitfire.synctools.exception.InvalidLocalResourceException +import net.fortuna.ical4j.data.CalendarOutputter +import net.fortuna.ical4j.model.Calendar +import net.fortuna.ical4j.model.TextList +import net.fortuna.ical4j.model.TimeZone +import net.fortuna.ical4j.model.component.VEvent +import net.fortuna.ical4j.model.property.Categories +import net.fortuna.ical4j.model.property.Color +import net.fortuna.ical4j.model.property.Description +import net.fortuna.ical4j.model.property.Location +import net.fortuna.ical4j.model.property.ProdId +import net.fortuna.ical4j.model.property.Sequence +import net.fortuna.ical4j.model.property.Summary +import net.fortuna.ical4j.model.property.Transp +import net.fortuna.ical4j.model.property.Uid +import net.fortuna.ical4j.model.property.Url +import net.fortuna.ical4j.model.property.Version +import java.io.OutputStream +import java.util.logging.Logger + +/** + * Writes an [Event] data class to a stream that contains an iCalendar + * (VCALENDAR with VEVENTs and optional VTIMEZONEs). + * + * @param prodId PRODID to use in iCalendar + */ +class EventWriter( + private val prodId: ProdId +) { + + private val logger: Logger + get() = Logger.getLogger(javaClass.name) + + + /** + * Generates an iCalendar from the Event. + * + * @param os stream that the iCalendar is written to + */ + fun write(event: Event, os: OutputStream) { + val ical = Calendar() + ical.properties += Version.VERSION_2_0 + ical.properties += prodId.withUserAgents(event.userAgents) + + val dtStart = event.dtStart ?: throw InvalidLocalResourceException("Won't generate event without start time") + + EventValidator.repair(event) // repair this event before creating the VEVENT + + // "main event" (without exceptions) + val components = ical.components + val mainEvent = toVEvent(event) + components += mainEvent + + // remember used time zones + val usedTimeZones = mutableSetOf() + dtStart.timeZone?.let(usedTimeZones::add) + event.dtEnd?.timeZone?.let(usedTimeZones::add) + + // recurrence exceptions + for (exception in event.exceptions) { + // exceptions must always have the same UID as the main event + exception.uid = event.uid + + val recurrenceId = exception.recurrenceId + if (recurrenceId == null) { + logger.warning("Ignoring exception without recurrenceId") + continue + } + + /* Exceptions must always have the same value type as DTSTART [RFC 5545 3.8.4.4]. + If this is not the case, we don't add the exception to the event because we're + strict in what we send (and servers may reject such a case). + */ + if (isDateTime(recurrenceId) != isDateTime(dtStart)) { + logger.warning("Ignoring exception $recurrenceId with other date type than dtStart: $dtStart") + continue + } + + // for simplicity and compatibility, rewrite date-time exceptions to the same time zone as DTSTART + if (isDateTime(recurrenceId) && recurrenceId.timeZone != dtStart.timeZone) { + logger.fine("Changing timezone of $recurrenceId to same time zone as dtStart: $dtStart") + recurrenceId.timeZone = dtStart.timeZone + } + + // create and add VEVENT for exception + val vException = toVEvent(exception) + components += vException + + // remember used time zones + exception.dtStart?.timeZone?.let(usedTimeZones::add) + exception.dtEnd?.timeZone?.let(usedTimeZones::add) + } + + // determine first dtStart (there may be exceptions with an earlier DTSTART that the main event) + val dtStarts = mutableListOf(dtStart.date) + dtStarts.addAll(event.exceptions.mapNotNull { it.dtStart?.date }) + val earliest = dtStarts.minOrNull() + // add VTIMEZONE components + for (tz in usedTimeZones) + ical.components += minifyVTimeZone(tz.vTimeZone, earliest) + + softValidate(ical) + CalendarOutputter(false).output(ical, os) + } + + /** + * Generates a VEvent representation of this event. + * + * @return generated VEvent + */ + private fun toVEvent(from: Event): VEvent { + val event = VEvent(/* generates DTSTAMP */) + val props = event.properties + props += Uid(from.uid) + + from.recurrenceId?.let { props += it } + from.sequence?.let { + if (it != 0) + props += Sequence(it) + } + + from.summary?.let { props += Summary(it) } + from.location?.let { props += Location(it) } + from.url?.let { props += Url(it) } + from.description?.let { props += Description(it) } + from.color?.let { props += Color(null, it.name) } + + from.dtStart?.let { props += it } + from.dtEnd?.let { props += it } + from.duration?.let { props += it } + + props.addAll(from.rRules) + props.addAll(from.rDates) + props.addAll(from.exRules) + props.addAll(from.exDates) + + from.classification?.let { props += it } + from.status?.let { props += it } + if (!from.opaque) + props += Transp.TRANSPARENT + + from.organizer?.let { props += it } + props.addAll(from.attendees) + + if (from.categories.isNotEmpty()) + props += Categories(TextList(from.categories.toTypedArray())) + props.addAll(from.unknownProperties) + + from.lastModified?.let { props += it } + + event.components.addAll(from.alarms) + + return event + } + +} \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/EventWriterTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/EventWriterTest.kt new file mode 100644 index 00000000..e46e0d8b --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/ical4android/EventWriterTest.kt @@ -0,0 +1,111 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.ical4android + +import net.fortuna.ical4j.model.Date +import net.fortuna.ical4j.model.DateTime +import net.fortuna.ical4j.model.TimeZoneRegistryFactory +import net.fortuna.ical4j.model.component.VAlarm +import net.fortuna.ical4j.model.property.Attendee +import net.fortuna.ical4j.model.property.DtEnd +import net.fortuna.ical4j.model.property.DtStart +import net.fortuna.ical4j.model.property.ProdId +import net.fortuna.ical4j.model.property.RRule +import net.fortuna.ical4j.model.property.RecurrenceId +import net.fortuna.ical4j.util.TimeZones +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.ByteArrayOutputStream +import java.time.Duration + +class EventWriterTest { + + private val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry() + private val tzBerlin = tzRegistry.getTimeZone("Europe/Berlin")!! + private val tzLondon = tzRegistry.getTimeZone("Europe/London")!! + private val tzUTC = tzRegistry.getTimeZone(TimeZones.UTC_ID)!! + + private val writer = EventWriter(prodId = ProdId(javaClass.name)) + + @Test + fun testGenerateEtcUTC() { + val e = Event() + e.uid = "etc-utc-test@example.com" + e.dtStart = DtStart("20200926T080000", tzUTC) + e.dtEnd = DtEnd("20200926T100000", tzUTC) + e.alarms += VAlarm(Duration.ofMinutes(-30)) + e.attendees += Attendee("mailto:test@example.com") + val baos = ByteArrayOutputStream() + writer.write(e, baos) + val ical = baos.toString() + + assertTrue( + "BEGIN:VTIMEZONE.+BEGIN:STANDARD.+END:STANDARD.+END:VTIMEZONE".toRegex(RegexOption.DOT_MATCHES_ALL).containsMatchIn(ical) + ) + } + + @Test + fun testRecurringWriteFullDayException() { + val event = Event().apply { + uid = "test1" + dtStart = DtStart("20190117T083000", tzBerlin) + summary = "Main event" + rRules += RRule("FREQ=DAILY;COUNT=5") + exceptions += arrayOf( + Event().apply { + uid = "test2" + recurrenceId = RecurrenceId(DateTime("20190118T073000", tzLondon)) + summary = "Normal exception" + }, + Event().apply { + uid = "test3" + recurrenceId = RecurrenceId(Date("20190223")) + summary = "Full-day exception" + } + ) + } + val baos = ByteArrayOutputStream() + writer.write(event, baos) + val iCal = baos.toString() + assertTrue(iCal.contains("UID:test1\r\n")) + assertTrue(iCal.contains("DTSTART;TZID=Europe/Berlin:20190117T083000\r\n")) + + // first RECURRENCE-ID has been rewritten + // - to main event's UID + // - to time zone Europe/Berlin (with one hour time difference) + assertTrue(iCal.contains("UID:test1\r\n" + + "RECURRENCE-ID;TZID=Europe/Berlin:20190118T083000\r\n" + + "SUMMARY:Normal exception\r\n" + + "END:VEVENT")) + + // no RECURRENCE-ID;VALUE=DATE:20190223 + assertFalse(iCal.contains(":20190223")) + } + + @Test + fun testWrite() { + val e = Event() + e.uid = "SAMPLEUID" + e.dtStart = DtStart("20190101T100000", tzBerlin) + e.alarms += VAlarm(Duration.ofHours(-1)) + + val os = ByteArrayOutputStream() + writer.write(e, os) + val raw = os.toString(Charsets.UTF_8.name()) + + assertTrue(raw.contains("PRODID:${javaClass.name}")) + assertTrue(raw.contains("UID:SAMPLEUID")) + assertTrue(raw.contains("DTSTART;TZID=Europe/Berlin:20190101T100000")) + assertTrue(raw.contains("DTSTAMP:")) + assertTrue(raw.contains("BEGIN:VALARM\r\n" + + "TRIGGER:-PT1H\r\n" + + "END:VALARM\r\n")) + assertTrue(raw.contains("BEGIN:VTIMEZONE")) + } + +} \ No newline at end of file From faa6a75c84751556ca88882310a8a2925ce0cc3e Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 9 Jul 2025 21:22:34 +0200 Subject: [PATCH 2/4] Extract EventReader --- .../kotlin/at/bitfire/ical4android/Event.kt | 139 +-------------- .../at/bitfire/ical4android/EventReader.kt | 159 ++++++++++++++++++ .../at/bitfire/ical4android/EventWriter.kt | 5 +- .../bitfire/ical4android/EventReaderTest.kt} | 58 +------ .../at/bitfire/ical4android/EventTest.kt | 46 +++++ .../validation/EventValidatorTest.kt | 18 +- 6 files changed, 232 insertions(+), 193 deletions(-) create mode 100644 lib/src/main/kotlin/at/bitfire/ical4android/EventReader.kt rename lib/src/{androidTest/kotlin/at/bitfire/ical4android/EventTest.kt => test/kotlin/at/bitfire/ical4android/EventReaderTest.kt} (82%) create mode 100644 lib/src/test/kotlin/at/bitfire/ical4android/EventTest.kt diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt index e346f2f4..7448cfc8 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt @@ -6,48 +6,33 @@ package at.bitfire.ical4android -import at.bitfire.ical4android.ICalendar.Companion.CALENDAR_NAME -import at.bitfire.ical4android.validation.EventValidator -import at.bitfire.synctools.icalendar.CalendarUidSplitter import at.bitfire.synctools.icalendar.Css3Color -import net.fortuna.ical4j.data.ParserException -import net.fortuna.ical4j.model.Component import net.fortuna.ical4j.model.Parameter import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.component.VAlarm -import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.parameter.Email import net.fortuna.ical4j.model.property.Attendee -import net.fortuna.ical4j.model.property.Categories import net.fortuna.ical4j.model.property.Clazz -import net.fortuna.ical4j.model.property.Color -import net.fortuna.ical4j.model.property.Description import net.fortuna.ical4j.model.property.DtEnd -import net.fortuna.ical4j.model.property.DtStamp import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.Duration import net.fortuna.ical4j.model.property.ExDate import net.fortuna.ical4j.model.property.ExRule import net.fortuna.ical4j.model.property.LastModified -import net.fortuna.ical4j.model.property.Location import net.fortuna.ical4j.model.property.Organizer -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.RecurrenceId -import net.fortuna.ical4j.model.property.Sequence import net.fortuna.ical4j.model.property.Status -import net.fortuna.ical4j.model.property.Summary -import net.fortuna.ical4j.model.property.Transp -import net.fortuna.ical4j.model.property.Uid -import net.fortuna.ical4j.model.property.Url -import java.io.IOException -import java.io.Reader import java.net.URI import java.util.LinkedList -import java.util.UUID -import java.util.logging.Logger +/** + * Data class that represents an event + * + * - as it is extracted from an iCalendar or + * - as it should be generated into an iCalendar. + */ data class Event( override var uid: String? = null, override var sequence: Int? = null, @@ -105,116 +90,4 @@ data class Event( return email } - - companion object { - - private val logger - get() = Logger.getLogger(Event::class.java.name) - - /** - * Parses an iCalendar resource, applies [at.bitfire.synctools.icalendar.validation.ICalPreprocessor] - * and [EventValidator] to increase compatibility and extracts the VEVENTs. - * - * @param reader where the iCalendar is read from - * @param properties Known iCalendar properties (like [CALENDAR_NAME]) will be put into this map. Key: property name; value: property value - * - * @return array of filled [Event] data objects (may have size 0) - * - * @throws IOException on I/O errors - * @throws ParserException when the iCalendar can't be parsed - */ - fun eventsFromReader( - reader: Reader, - properties: MutableMap? = null - ): List { - val ical = fromReader(reader, properties) - - // process VEVENTs - val splitter = CalendarUidSplitter() - val vEventsByUid = splitter.associateByUid(ical, Component.VEVENT) - - /* Note: There may be UIDs which have only RECURRENCE-ID entries and not a main entry (for instance, a recurring - event with an exception where the current user has been invited only to this exception. In this case, - the UID will not appear in mainEvents but only in exceptions. */ - - // make sure every event has an UID - vEventsByUid[null]?.let { withoutUid -> - val uid = Uid(UUID.randomUUID().toString()) - logger.warning("Found VEVENT without UID, using a random one: ${uid.value}") - withoutUid.main?.properties?.add(uid) - withoutUid.exceptions.forEach { it.properties.add(uid) } - } - - // convert into Events (data class) - val events = mutableListOf() - for (associatedEvents in vEventsByUid.values) { - val mainVEvent = associatedEvents.main ?: - // no main event but only exceptions, create fake main event - // FIXME: we should construct a proper recurring fake event, not just take first the exception - associatedEvents.exceptions.first() - - val event = fromVEvent(mainVEvent) - associatedEvents.exceptions.mapTo(event.exceptions) { exceptionVEvent -> - fromVEvent(exceptionVEvent).also { exception -> - // make sure that exceptions have at least a SUMMARY (if the main event does have one) - if (exception.summary == null) - exception.summary = event.summary - } - } - - events += event - } - - // Try to repair all events after reading the whole iCalendar - for (event in events) - EventValidator.repair(event) - - return events - } - - fun fromVEvent(event: VEvent): Event { - val e = Event() - - // sequence must only be null for locally created, not-yet-synchronized events - e.sequence = 0 - - // process properties - for (prop in event.properties) - when (prop) { - is Uid -> e.uid = prop.value - is RecurrenceId -> e.recurrenceId = prop - is Sequence -> e.sequence = prop.sequenceNo - is Summary -> e.summary = prop.value - is Location -> e.location = prop.value - is Url -> e.url = prop.uri - is Description -> e.description = prop.value - is Categories -> - for (category in prop.categories) - e.categories += category - - is Color -> e.color = Css3Color.fromString(prop.value) - is DtStart -> e.dtStart = prop - is DtEnd -> e.dtEnd = prop - is Duration -> e.duration = prop - is RRule -> e.rRules += prop - is RDate -> e.rDates += prop - is ExRule -> e.exRules += prop - is ExDate -> e.exDates += prop - is Clazz -> e.classification = prop - is Status -> e.status = prop - is Transp -> e.opaque = prop == Transp.OPAQUE - is Organizer -> e.organizer = prop - is Attendee -> e.attendees += prop - is LastModified -> e.lastModified = prop - is ProdId, is DtStamp -> { /* don't save these as unknown properties */ } - - else -> e.unknownProperties += prop - } - - e.alarms.addAll(event.alarms) - - return e - } - } - } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/EventReader.kt b/lib/src/main/kotlin/at/bitfire/ical4android/EventReader.kt new file mode 100644 index 00000000..cadf9957 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/ical4android/EventReader.kt @@ -0,0 +1,159 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.ical4android + +import at.bitfire.ical4android.ICalendar.Companion.CALENDAR_NAME +import at.bitfire.ical4android.ICalendar.Companion.fromReader +import at.bitfire.ical4android.validation.EventValidator +import at.bitfire.synctools.icalendar.CalendarUidSplitter +import at.bitfire.synctools.icalendar.Css3Color +import net.fortuna.ical4j.data.ParserException +import net.fortuna.ical4j.model.Component +import net.fortuna.ical4j.model.component.VEvent +import net.fortuna.ical4j.model.property.Attendee +import net.fortuna.ical4j.model.property.Categories +import net.fortuna.ical4j.model.property.Clazz +import net.fortuna.ical4j.model.property.Color +import net.fortuna.ical4j.model.property.Description +import net.fortuna.ical4j.model.property.DtEnd +import net.fortuna.ical4j.model.property.DtStamp +import net.fortuna.ical4j.model.property.DtStart +import net.fortuna.ical4j.model.property.Duration +import net.fortuna.ical4j.model.property.ExDate +import net.fortuna.ical4j.model.property.ExRule +import net.fortuna.ical4j.model.property.LastModified +import net.fortuna.ical4j.model.property.Location +import net.fortuna.ical4j.model.property.Organizer +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.RecurrenceId +import net.fortuna.ical4j.model.property.Sequence +import net.fortuna.ical4j.model.property.Status +import net.fortuna.ical4j.model.property.Summary +import net.fortuna.ical4j.model.property.Transp +import net.fortuna.ical4j.model.property.Uid +import net.fortuna.ical4j.model.property.Url +import java.io.IOException +import java.io.Reader +import java.util.UUID +import java.util.logging.Logger + +/** + * Generates an [Event] from an iCalendar in a [Reader] source. + */ +class EventReader { + + private val logger + get() = Logger.getLogger(Event::class.java.name) + + /** + * Parses an iCalendar resource, applies [at.bitfire.synctools.icalendar.validation.ICalPreprocessor] + * and [EventValidator] to increase compatibility and extracts the VEVENTs. + * + * @param reader where the iCalendar is read from + * @param properties Known iCalendar properties (like [CALENDAR_NAME]) will be put into this map. Key: property name; value: property value + * + * @return array of filled [Event] data objects (may have size 0) + * + * @throws IOException on I/O errors + * @throws ParserException when the iCalendar can't be parsed + */ + fun eventsFromReader( + reader: Reader, + properties: MutableMap? = null + ): List { + val ical = fromReader(reader, properties) + + // process VEVENTs + val splitter = CalendarUidSplitter() + val vEventsByUid = splitter.associateByUid(ical, Component.VEVENT) + + /* Note: There may be UIDs which have only RECURRENCE-ID entries and not a main entry (for instance, a recurring + event with an exception where the current user has been invited only to this exception. In this case, + the UID will not appear in mainEvents but only in exceptions. */ + + // make sure every event has an UID + vEventsByUid[null]?.let { withoutUid -> + val uid = Uid(UUID.randomUUID().toString()) + logger.warning("Found VEVENT without UID, using a random one: ${uid.value}") + withoutUid.main?.properties?.add(uid) + withoutUid.exceptions.forEach { it.properties.add(uid) } + } + + // convert into Events (data class) + val events = mutableListOf() + for (associatedEvents in vEventsByUid.values) { + val mainVEvent = associatedEvents.main ?: + // no main event but only exceptions, create fake main event + // FIXME: we should construct a proper recurring fake event, not just take first the exception + associatedEvents.exceptions.first() + + val event = fromVEvent(mainVEvent) + associatedEvents.exceptions.mapTo(event.exceptions) { exceptionVEvent -> + fromVEvent(exceptionVEvent).also { exception -> + // make sure that exceptions have at least a SUMMARY (if the main event does have one) + if (exception.summary == null) + exception.summary = event.summary + } + } + + events += event + } + + // Try to repair all events after reading the whole iCalendar + for (event in events) + EventValidator.repair(event) + + return events + } + + fun fromVEvent(event: VEvent): Event { + val e = Event() + + // sequence must only be null for locally created, not-yet-synchronized events + e.sequence = 0 + + // process properties + for (prop in event.properties) + when (prop) { + is Uid -> e.uid = prop.value + is RecurrenceId -> e.recurrenceId = prop + is Sequence -> e.sequence = prop.sequenceNo + is Summary -> e.summary = prop.value + is Location -> e.location = prop.value + is Url -> e.url = prop.uri + is Description -> e.description = prop.value + is Categories -> + for (category in prop.categories) + e.categories += category + + is Color -> e.color = Css3Color.fromString(prop.value) + is DtStart -> e.dtStart = prop + is DtEnd -> e.dtEnd = prop + is Duration -> e.duration = prop + is RRule -> e.rRules += prop + is RDate -> e.rDates += prop + is ExRule -> e.exRules += prop + is ExDate -> e.exDates += prop + is Clazz -> e.classification = prop + is Status -> e.status = prop + is Transp -> e.opaque = prop == Transp.OPAQUE + is Organizer -> e.organizer = prop + is Attendee -> e.attendees += prop + is LastModified -> e.lastModified = prop + is ProdId, is DtStamp -> { /* don't save these as unknown properties */ } + + else -> e.unknownProperties += prop + } + + e.alarms.addAll(event.alarms) + + return e + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/EventWriter.kt b/lib/src/main/kotlin/at/bitfire/ical4android/EventWriter.kt index 3bd5683f..dfec3af6 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/EventWriter.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/EventWriter.kt @@ -46,9 +46,10 @@ class EventWriter( /** - * Generates an iCalendar from the Event. + * Applies error correction over [EventValidator] to an [Event] and generates an iCalendar from it. * - * @param os stream that the iCalendar is written to + * @param event event to generate iCalendar from + * @param os stream that the iCalendar is written to */ fun write(event: Event, os: OutputStream) { val ical = Calendar() diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/EventReaderTest.kt similarity index 82% rename from lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt rename to lib/src/test/kotlin/at/bitfire/ical4android/EventReaderTest.kt index a1a89844..9d59933b 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/EventReaderTest.kt @@ -9,10 +9,7 @@ package at.bitfire.ical4android import at.bitfire.ical4android.util.DateUtils import at.bitfire.synctools.icalendar.Css3Color import net.fortuna.ical4j.model.Parameter -import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.parameter.Email -import net.fortuna.ical4j.model.property.Organizer -import net.fortuna.ical4j.util.TimeZones import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue @@ -21,20 +18,15 @@ import java.io.FileNotFoundException import java.io.InputStreamReader import java.nio.charset.Charset -class EventTest { +class EventReaderTest { - private val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry() - private val tzBerlin = tzRegistry.getTimeZone("Europe/Berlin")!! - private val tzLondon = tzRegistry.getTimeZone("Europe/London")!! - private val tzUTC = tzRegistry.getTimeZone(TimeZones.UTC_ID)!! - - /* public interface tests */ + val reader = EventReader() @Test fun testCalendarProperties() { - javaClass.classLoader!!.getResourceAsStream("events/multiple.ics").use { stream -> + javaClass.getResourceAsStream("/events/multiple.ics").use { stream -> val properties = mutableMapOf() - Event.eventsFromReader(InputStreamReader(stream, Charsets.UTF_8), properties) + reader.eventsFromReader(InputStreamReader(stream, Charsets.UTF_8), properties) assertEquals(1, properties.size) assertEquals("Test-Kalender", properties[ICalendar.CALENDAR_NAME]) } @@ -173,19 +165,7 @@ class EventTest { } @Test - fun testToString() { - val e = Event() - e.uid = "SAMPLEUID" - val s = e.toString() - assertTrue(s.contains(Event::class.java.simpleName)) - assertTrue(s.contains("uid=SAMPLEUID")) - } - - - /* internal tests */ - - @Test - fun testFindMasterEventsAndExceptions() { + fun testFindMainEventsAndExceptions() { // two single events var events = parseCalendar("two-events-without-exceptions.ics") assertEquals(2, events.size) @@ -221,30 +201,6 @@ class EventTest { } - // methods / fields - - @Test - fun testOrganizerEmail_None() { - assertNull(Event().organizerEmail) - } - - @Test - fun testOrganizerEmail_EmailParameter() { - assertEquals("organizer@example.com", Event().apply { - organizer = Organizer("SomeFancyOrganizer").apply { - parameters.add(Email("organizer@example.com")) - } - }.organizerEmail) - } - - @Test - fun testOrganizerEmail_MailtoValue() { - assertEquals("organizer@example.com", Event().apply { - organizer = Organizer("mailto:organizer@example.com") - }.organizerEmail) - } - - // helpers private fun findEvent(events: Iterable, uid: String): Event { @@ -255,8 +211,8 @@ class EventTest { } private fun parseCalendar(fname: String, charset: Charset = Charsets.UTF_8): List = - javaClass.classLoader!!.getResourceAsStream("events/$fname").use { stream -> - return Event.eventsFromReader(InputStreamReader(stream, charset)) + javaClass.getResourceAsStream("/events/$fname").use { stream -> + return reader.eventsFromReader(InputStreamReader(stream, charset)) } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/EventTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/EventTest.kt new file mode 100644 index 00000000..43b9daa3 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/ical4android/EventTest.kt @@ -0,0 +1,46 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.ical4android + +import net.fortuna.ical4j.model.parameter.Email +import net.fortuna.ical4j.model.property.Organizer +import org.junit.Assert +import org.junit.Test + +class EventTest { + + @Test + fun testToString() { + val e = Event() + e.uid = "SAMPLEUID" + val s = e.toString() + Assert.assertTrue(s.contains(Event::class.java.simpleName)) + Assert.assertTrue(s.contains("uid=SAMPLEUID")) + } + + @Test + fun testOrganizerEmail_None() { + Assert.assertNull(Event().organizerEmail) + } + + @Test + fun testOrganizerEmail_EmailParameter() { + Assert.assertEquals("organizer@example.com", Event().apply { + organizer = Organizer("SomeFancyOrganizer").apply { + parameters.add(Email("organizer@example.com")) + } + }.organizerEmail) + } + + @Test + fun testOrganizerEmail_MailtoValue() { + Assert.assertEquals("organizer@example.com", Event().apply { + organizer = Organizer("mailto:organizer@example.com") + }.organizerEmail) + } + +} \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt index a1a12228..62ca832b 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt @@ -7,6 +7,7 @@ package at.bitfire.ical4android.validation import at.bitfire.ical4android.Event +import at.bitfire.ical4android.EventReader import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateList import net.fortuna.ical4j.model.DateTime @@ -38,6 +39,8 @@ class EventValidatorTest { companion object { val tzReg: TimeZoneRegistry = TimeZoneRegistryFactory.getInstance().createRegistry() } + + val eventReader = EventReader() // DTSTART and DTEND @@ -83,7 +86,7 @@ class EventValidatorTest { EventValidator.correctStartAndEndTime(event) assertNull(event.dtEnd) - val event1 = Event.eventsFromReader(StringReader( + val event1 = eventReader.eventsFromReader(StringReader( "BEGIN:VCALENDAR\n" + "BEGIN:VEVENT\n" + "UID:51d8529a-5844-4609-918b-2891b855e0e8\n" + @@ -111,7 +114,8 @@ class EventValidatorTest { assertEquals(DateTime("20211115T001100Z"), event.dtStart!!.date) assertEquals("FREQ=MONTHLY;UNTIL=20251214T001100Z", event.rRules.joinToString()) - val event1 = Event.eventsFromReader(StringReader( + val eventReader = EventReader() + val event1 = eventReader.eventsFromReader(StringReader( "BEGIN:VCALENDAR\n" + "BEGIN:VEVENT\n" + "UID:51d8529a-5844-4609-918b-2891b855e0e8\n" + @@ -121,7 +125,7 @@ class EventValidatorTest { "END:VCALENDAR")).first() assertEquals("FREQ=MONTHLY;UNTIL=20231214;BYMONTHDAY=15", event1.rRules.joinToString()) - val event2 = Event.eventsFromReader(StringReader( + val event2 = eventReader.eventsFromReader(StringReader( "BEGIN:VCALENDAR\n" + "BEGIN:VEVENT\n" + "UID:381fb26b-2da5-4dd2-94d7-2e0874128aa7\n" + @@ -148,7 +152,7 @@ class EventValidatorTest { EventValidator.sameTypeForDtStartAndRruleUntil(event.dtStart!!, event.rRules) assertEquals("FREQ=MONTHLY;UNTIL=20211214", event.rRules.joinToString()) - val event1 = Event.eventsFromReader( + val event1 = eventReader.eventsFromReader( StringReader( "BEGIN:VCALENDAR\n" + "BEGIN:VEVENT\n" + @@ -194,7 +198,7 @@ class EventValidatorTest { EventValidator.sameTypeForDtStartAndRruleUntil(event.dtStart!!, event.rRules) assertEquals("FREQ=MONTHLY;UNTIL=20211214T001100Z", event.rRules.joinToString()) - val event1 = Event.eventsFromReader(StringReader( + val event1 = eventReader.eventsFromReader(StringReader( "BEGIN:VCALENDAR\n" + "BEGIN:VEVENT\n" + "UID:51d8529a-5844-4609-918b-2891b855e0e8\n" + @@ -217,7 +221,7 @@ class EventValidatorTest { EventValidator.sameTypeForDtStartAndRruleUntil(event.dtStart!!, event.rRules) assertEquals("FREQ=MONTHLY;UNTIL=20211214T001100Z", event.rRules.joinToString()) - val event2 = Event.eventsFromReader( + val event2 = eventReader.eventsFromReader( StringReader( "BEGIN:VCALENDAR\n" + "BEGIN:VEVENT\n" + @@ -449,7 +453,7 @@ class EventValidatorTest { assertTrue(manualEvent.exceptions.first().exDates.isEmpty()) // Test event from reader, the reader will repair the event itself - val eventFromReader = Event.eventsFromReader(StringReader( + val eventFromReader = eventReader.eventsFromReader(StringReader( "BEGIN:VCALENDAR\n" + "BEGIN:VEVENT\n" + "DTSTAMP:20240215T102755Z\n" + From 34b659940a6d3420a32ea19d4e5d78cf50ca6cda Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 9 Jul 2025 21:36:44 +0200 Subject: [PATCH 3/4] Use Writer for EventWriter --- .../kotlin/at/bitfire/ical4android/EventReader.kt | 8 ++++---- .../kotlin/at/bitfire/ical4android/EventWriter.kt | 8 ++++---- .../at/bitfire/ical4android/EventReaderTest.kt | 4 ++-- .../ical4android/validation/EventValidatorTest.kt | 14 +++++++------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/EventReader.kt b/lib/src/main/kotlin/at/bitfire/ical4android/EventReader.kt index cadf9957..5c1fef33 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/EventReader.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/EventReader.kt @@ -55,7 +55,7 @@ class EventReader { * Parses an iCalendar resource, applies [at.bitfire.synctools.icalendar.validation.ICalPreprocessor] * and [EventValidator] to increase compatibility and extracts the VEVENTs. * - * @param reader where the iCalendar is read from + * @param from where the iCalendar is read from * @param properties Known iCalendar properties (like [CALENDAR_NAME]) will be put into this map. Key: property name; value: property value * * @return array of filled [Event] data objects (may have size 0) @@ -63,11 +63,11 @@ class EventReader { * @throws IOException on I/O errors * @throws ParserException when the iCalendar can't be parsed */ - fun eventsFromReader( - reader: Reader, + fun readEvents( + from: Reader, properties: MutableMap? = null ): List { - val ical = fromReader(reader, properties) + val ical = fromReader(from, properties) // process VEVENTs val splitter = CalendarUidSplitter() diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/EventWriter.kt b/lib/src/main/kotlin/at/bitfire/ical4android/EventWriter.kt index dfec3af6..07e6160e 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/EventWriter.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/EventWriter.kt @@ -28,7 +28,7 @@ import net.fortuna.ical4j.model.property.Transp import net.fortuna.ical4j.model.property.Uid import net.fortuna.ical4j.model.property.Url import net.fortuna.ical4j.model.property.Version -import java.io.OutputStream +import java.io.Writer import java.util.logging.Logger /** @@ -49,9 +49,9 @@ class EventWriter( * Applies error correction over [EventValidator] to an [Event] and generates an iCalendar from it. * * @param event event to generate iCalendar from - * @param os stream that the iCalendar is written to + * @param to stream that the iCalendar is written to */ - fun write(event: Event, os: OutputStream) { + fun write(event: Event, to: Writer) { val ical = Calendar() ical.properties += Version.VERSION_2_0 ical.properties += prodId.withUserAgents(event.userAgents) @@ -114,7 +114,7 @@ class EventWriter( ical.components += minifyVTimeZone(tz.vTimeZone, earliest) softValidate(ical) - CalendarOutputter(false).output(ical, os) + CalendarOutputter(false).output(ical, to) } /** diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/EventReaderTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/EventReaderTest.kt index 9d59933b..8b070e50 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/EventReaderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/EventReaderTest.kt @@ -26,7 +26,7 @@ class EventReaderTest { fun testCalendarProperties() { javaClass.getResourceAsStream("/events/multiple.ics").use { stream -> val properties = mutableMapOf() - reader.eventsFromReader(InputStreamReader(stream, Charsets.UTF_8), properties) + reader.readEvents(InputStreamReader(stream, Charsets.UTF_8), properties) assertEquals(1, properties.size) assertEquals("Test-Kalender", properties[ICalendar.CALENDAR_NAME]) } @@ -212,7 +212,7 @@ class EventReaderTest { private fun parseCalendar(fname: String, charset: Charset = Charsets.UTF_8): List = javaClass.getResourceAsStream("/events/$fname").use { stream -> - return reader.eventsFromReader(InputStreamReader(stream, charset)) + return reader.readEvents(InputStreamReader(stream, charset)) } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt index 62ca832b..e8953270 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt @@ -86,7 +86,7 @@ class EventValidatorTest { EventValidator.correctStartAndEndTime(event) assertNull(event.dtEnd) - val event1 = eventReader.eventsFromReader(StringReader( + val event1 = eventReader.readEvents(StringReader( "BEGIN:VCALENDAR\n" + "BEGIN:VEVENT\n" + "UID:51d8529a-5844-4609-918b-2891b855e0e8\n" + @@ -115,7 +115,7 @@ class EventValidatorTest { assertEquals("FREQ=MONTHLY;UNTIL=20251214T001100Z", event.rRules.joinToString()) val eventReader = EventReader() - val event1 = eventReader.eventsFromReader(StringReader( + val event1 = eventReader.readEvents(StringReader( "BEGIN:VCALENDAR\n" + "BEGIN:VEVENT\n" + "UID:51d8529a-5844-4609-918b-2891b855e0e8\n" + @@ -125,7 +125,7 @@ class EventValidatorTest { "END:VCALENDAR")).first() assertEquals("FREQ=MONTHLY;UNTIL=20231214;BYMONTHDAY=15", event1.rRules.joinToString()) - val event2 = eventReader.eventsFromReader(StringReader( + val event2 = eventReader.readEvents(StringReader( "BEGIN:VCALENDAR\n" + "BEGIN:VEVENT\n" + "UID:381fb26b-2da5-4dd2-94d7-2e0874128aa7\n" + @@ -152,7 +152,7 @@ class EventValidatorTest { EventValidator.sameTypeForDtStartAndRruleUntil(event.dtStart!!, event.rRules) assertEquals("FREQ=MONTHLY;UNTIL=20211214", event.rRules.joinToString()) - val event1 = eventReader.eventsFromReader( + val event1 = eventReader.readEvents( StringReader( "BEGIN:VCALENDAR\n" + "BEGIN:VEVENT\n" + @@ -198,7 +198,7 @@ class EventValidatorTest { EventValidator.sameTypeForDtStartAndRruleUntil(event.dtStart!!, event.rRules) assertEquals("FREQ=MONTHLY;UNTIL=20211214T001100Z", event.rRules.joinToString()) - val event1 = eventReader.eventsFromReader(StringReader( + val event1 = eventReader.readEvents(StringReader( "BEGIN:VCALENDAR\n" + "BEGIN:VEVENT\n" + "UID:51d8529a-5844-4609-918b-2891b855e0e8\n" + @@ -221,7 +221,7 @@ class EventValidatorTest { EventValidator.sameTypeForDtStartAndRruleUntil(event.dtStart!!, event.rRules) assertEquals("FREQ=MONTHLY;UNTIL=20211214T001100Z", event.rRules.joinToString()) - val event2 = eventReader.eventsFromReader( + val event2 = eventReader.readEvents( StringReader( "BEGIN:VCALENDAR\n" + "BEGIN:VEVENT\n" + @@ -453,7 +453,7 @@ class EventValidatorTest { assertTrue(manualEvent.exceptions.first().exDates.isEmpty()) // Test event from reader, the reader will repair the event itself - val eventFromReader = eventReader.eventsFromReader(StringReader( + val eventFromReader = eventReader.readEvents(StringReader( "BEGIN:VCALENDAR\n" + "BEGIN:VEVENT\n" + "DTSTAMP:20240215T102755Z\n" + From fbca855961cc9092bedce37b3e6e5cc28d5a9f37 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 9 Jul 2025 21:43:49 +0200 Subject: [PATCH 4/4] Fix tests --- .../at/bitfire/ical4android/Ical4jTest.kt | 2 +- .../at/bitfire/ical4android/EventReader.kt | 2 +- .../bitfire/ical4android/EventWriterTest.kt | 24 ++++++++++--------- .../validation/EventValidatorTest.kt | 1 - 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/Ical4jTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/Ical4jTest.kt index c9fd4cab..8c133313 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/Ical4jTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/Ical4jTest.kt @@ -28,7 +28,7 @@ class Ical4jTest { @Test fun testEmailParameter() { // https://github.com/ical4j/ical4j/issues/418 - val e = Event.eventsFromReader( + val e = EventReader().readEvents( StringReader( "BEGIN:VCALENDAR\n" + "VERSION:2.0\n" + diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/EventReader.kt b/lib/src/main/kotlin/at/bitfire/ical4android/EventReader.kt index 5c1fef33..d1aae654 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/EventReader.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/EventReader.kt @@ -49,7 +49,7 @@ import java.util.logging.Logger class EventReader { private val logger - get() = Logger.getLogger(Event::class.java.name) + get() = Logger.getLogger(javaClass.name) /** * Parses an iCalendar resource, applies [at.bitfire.synctools.icalendar.validation.ICalPreprocessor] diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/EventWriterTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/EventWriterTest.kt index e46e0d8b..6152a808 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/EventWriterTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/EventWriterTest.kt @@ -20,7 +20,7 @@ import net.fortuna.ical4j.util.TimeZones import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test -import java.io.ByteArrayOutputStream +import java.io.StringWriter import java.time.Duration class EventWriterTest { @@ -40,12 +40,13 @@ class EventWriterTest { e.dtEnd = DtEnd("20200926T100000", tzUTC) e.alarms += VAlarm(Duration.ofMinutes(-30)) e.attendees += Attendee("mailto:test@example.com") - val baos = ByteArrayOutputStream() - writer.write(e, baos) - val ical = baos.toString() + val ical = StringWriter() + writer.write(e, ical) assertTrue( - "BEGIN:VTIMEZONE.+BEGIN:STANDARD.+END:STANDARD.+END:VTIMEZONE".toRegex(RegexOption.DOT_MATCHES_ALL).containsMatchIn(ical) + "BEGIN:VTIMEZONE.+BEGIN:STANDARD.+END:STANDARD.+END:VTIMEZONE" + .toRegex(RegexOption.DOT_MATCHES_ALL) + .containsMatchIn(ical.toString()) ) } @@ -69,9 +70,10 @@ class EventWriterTest { } ) } - val baos = ByteArrayOutputStream() - writer.write(event, baos) - val iCal = baos.toString() + val iCal = StringWriter().let { + writer.write(event, it) + it.toString() + } assertTrue(iCal.contains("UID:test1\r\n")) assertTrue(iCal.contains("DTSTART;TZID=Europe/Berlin:20190117T083000\r\n")) @@ -94,9 +96,9 @@ class EventWriterTest { e.dtStart = DtStart("20190101T100000", tzBerlin) e.alarms += VAlarm(Duration.ofHours(-1)) - val os = ByteArrayOutputStream() - writer.write(e, os) - val raw = os.toString(Charsets.UTF_8.name()) + val iCal = StringWriter() + writer.write(e, iCal) + val raw = iCal.toString() assertTrue(raw.contains("PRODID:${javaClass.name}")) assertTrue(raw.contains("UID:SAMPLEUID")) diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt index e8953270..94d253a5 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt @@ -114,7 +114,6 @@ class EventValidatorTest { assertEquals(DateTime("20211115T001100Z"), event.dtStart!!.date) assertEquals("FREQ=MONTHLY;UNTIL=20251214T001100Z", event.rRules.joinToString()) - val eventReader = EventReader() val event1 = eventReader.readEvents(StringReader( "BEGIN:VCALENDAR\n" + "BEGIN:VEVENT\n" +