Skip to content

Commit 5ff8e41

Browse files
authored
Remove EventValidator (#107)
* [WIP] StartTimeProcessor * [WIP] DurationProcessor * [WIP] Remove deprecated TimeFieldsProcessor and split into StartTimeProcessor, EndTimeProcessor, and DurationProcessor * Add DurationProcessorTest * Minor changes * Fix tests * Remove EventValidator and integrate its functionality into RecurrenceFieldsProcessor. * Make sure UNTIL in RRULE is in UTC format * Update AndroidTimeUtils RDATE/EXDATE documentation * Remove unnecessary UTC conversion for DATE-TIME UNTIL values
1 parent 1a7f70b commit 5ff8e41

File tree

9 files changed

+174
-413
lines changed

9 files changed

+174
-413
lines changed

lib/src/main/kotlin/at/bitfire/ical4android/EventReader.kt

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ package at.bitfire.ical4android
88

99
import at.bitfire.ical4android.ICalendar.Companion.CALENDAR_NAME
1010
import at.bitfire.ical4android.ICalendar.Companion.fromReader
11-
import at.bitfire.ical4android.validation.EventValidator
1211
import at.bitfire.synctools.icalendar.CalendarUidSplitter
1312
import at.bitfire.synctools.icalendar.Css3Color
1413
import net.fortuna.ical4j.data.ParserException
@@ -52,8 +51,7 @@ class EventReader {
5251
get() = Logger.getLogger(javaClass.name)
5352

5453
/**
55-
* Parses an iCalendar resource, applies [at.bitfire.synctools.icalendar.validation.ICalPreprocessor]
56-
* and [EventValidator] to increase compatibility and extracts the VEVENTs.
54+
* Parses an iCalendar resource and applies [at.bitfire.synctools.icalendar.validation.ICalPreprocessor].
5755
*
5856
* @param from where the iCalendar is read from
5957
* @param properties Known iCalendar properties (like [CALENDAR_NAME]) will be put into this map. Key: property name; value: property value
@@ -105,10 +103,6 @@ class EventReader {
105103
events += event
106104
}
107105

108-
// Try to repair all events after reading the whole iCalendar
109-
for (event in events)
110-
EventValidator.repair(event)
111-
112106
return events
113107
}
114108

lib/src/main/kotlin/at/bitfire/ical4android/EventWriter.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import at.bitfire.ical4android.ICalendar.Companion.minifyVTimeZone
1010
import at.bitfire.ical4android.ICalendar.Companion.softValidate
1111
import at.bitfire.ical4android.ICalendar.Companion.withUserAgents
1212
import at.bitfire.ical4android.util.DateUtils.isDateTime
13-
import at.bitfire.ical4android.validation.EventValidator
1413
import at.bitfire.synctools.exception.InvalidLocalResourceException
1514
import net.fortuna.ical4j.data.CalendarOutputter
1615
import net.fortuna.ical4j.model.Calendar
@@ -46,7 +45,7 @@ class EventWriter(
4645

4746

4847
/**
49-
* Applies error correction over [EventValidator] to an [Event] and generates an iCalendar from it.
48+
* Generates an iCalendar from the given [Event].
5049
*
5150
* @param event event to generate iCalendar from
5251
* @param to stream that the iCalendar is written to
@@ -58,8 +57,6 @@ class EventWriter(
5857

5958
val dtStart = event.dtStart ?: throw InvalidLocalResourceException("Won't generate event without start time")
6059

61-
EventValidator.repair(event) // repair this event before creating the VEVENT
62-
6360
// "main event" (without exceptions)
6461
val components = ical.components
6562
val mainEvent = toVEvent(event)

lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt

Lines changed: 0 additions & 169 deletions
This file was deleted.

lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilder.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ class EndTimeBuilder: AndroidEntityBuilder {
8686
* - DTEND and DTSTART are both either DATE or DATE-TIME → original DTEND
8787
* - DTEND is DATE, DTSTART is DATE-TIME → DTEND is amended to DATE-TIME with time and timezone from DTSTART
8888
* - DTEND is DATE-TIME, DTSTART is DATE → DTEND is reduced to its date component
89+
*
90+
* @see at.bitfire.synctools.mapping.calendar.processor.RecurrenceFieldsProcessor.alignUntil
8991
*/
9092
@VisibleForTesting
9193
internal fun alignWithDtStart(dtEnd: DtEnd, dtStart: DtStart): DtEnd {

lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/DurationProcessor.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class DurationProcessor(
6767
val start = startDateTime.toZonedDateTime()
6868
val end = start + duration
6969

70-
to.dtEnd = DtEnd(end.toIcal4jDateTime())
70+
to.dtEnd = DtEnd(end.toIcal4jDateTime(tzRegistry))
7171
}
7272
}
7373

lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/RecurrenceFieldsProcessor.kt

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,21 @@ package at.bitfire.synctools.mapping.calendar.processor
99
import android.content.Entity
1010
import android.provider.CalendarContract.Events
1111
import at.bitfire.ical4android.Event
12+
import at.bitfire.ical4android.util.TimeApiExtensions.toIcal4jDate
13+
import at.bitfire.ical4android.util.TimeApiExtensions.toIcal4jDateTime
14+
import at.bitfire.ical4android.util.TimeApiExtensions.toLocalDate
15+
import at.bitfire.ical4android.util.TimeApiExtensions.toZonedDateTime
1216
import at.bitfire.synctools.exception.InvalidLocalResourceException
1317
import at.bitfire.synctools.util.AndroidTimeUtils
18+
import net.fortuna.ical4j.model.Date
19+
import net.fortuna.ical4j.model.DateTime
20+
import net.fortuna.ical4j.model.Recur
1421
import net.fortuna.ical4j.model.TimeZoneRegistry
1522
import net.fortuna.ical4j.model.property.ExDate
1623
import net.fortuna.ical4j.model.property.ExRule
1724
import net.fortuna.ical4j.model.property.RDate
1825
import net.fortuna.ical4j.model.property.RRule
26+
import java.time.ZonedDateTime
1927
import java.util.LinkedList
2028
import java.util.logging.Level
2129
import java.util.logging.Logger
@@ -33,13 +41,26 @@ class RecurrenceFieldsProcessor(
3341
val tsStart = values.getAsLong(Events.DTSTART) ?: throw InvalidLocalResourceException("Found event without DTSTART")
3442
val allDay = (values.getAsInteger(Events.ALL_DAY) ?: 0) != 0
3543

44+
// provide start date as ical4j Date, if needed
45+
val startDate by lazy {
46+
AndroidTimeField(
47+
timestamp = tsStart,
48+
timeZone = values.getAsString(Events.EVENT_TIMEZONE),
49+
allDay = allDay,
50+
tzRegistry = tzRegistry
51+
).asIcal4jDate()
52+
}
53+
3654
// process RRULE field
3755
val rRules = LinkedList<RRule>()
3856
values.getAsString(Events.RRULE)?.let { rRuleField ->
3957
try {
4058
for (rule in rRuleField.split(AndroidTimeUtils.RECURRENCE_RULE_SEPARATOR)) {
4159
val rule = RRule(rule)
4260

61+
// align RRULE UNTIL to DTSTART, if needed
62+
rule.recur = alignUntil(rule.recur, startDate)
63+
4364
// skip if UNTIL is before event's DTSTART
4465
val tsUntil = rule.recur.until?.time
4566
if (tsUntil != null && tsUntil <= tsStart) {
@@ -74,6 +95,9 @@ class RecurrenceFieldsProcessor(
7495
for (rule in exRuleField.split(AndroidTimeUtils.RECURRENCE_RULE_SEPARATOR)) {
7596
val rule = ExRule(null, rule)
7697

98+
// align RRULE UNTIL to DTSTART, if needed
99+
rule.recur = alignUntil(rule.recur, startDate)
100+
77101
// skip if UNTIL is before event's DTSTART
78102
val tsUntil = rule.recur.until?.time
79103
if (tsUntil != null && tsUntil <= tsStart) {
@@ -109,4 +133,58 @@ class RecurrenceFieldsProcessor(
109133
}
110134
}
111135

136+
/**
137+
* Aligns the `UNTIL` of the given recurrence info to the VALUE-type (DATE-TIME/DATE) of [startDate].
138+
*
139+
* If the aligned `UNTIL` is a DATE-TIME, this method also makes sure that it's specified in UTC format
140+
* as required by RFC 5545 3.3.10.
141+
*
142+
* @param recur recurrence info whose `UNTIL` shall be aligned
143+
* @param startDate `DTSTART` date to compare with
144+
*
145+
* @return
146+
*
147+
* - UNTIL not set → original recur
148+
* - UNTIL and DTSTART are both either DATE or DATE-TIME → original recur
149+
* - UNTIL is DATE, DTSTART is DATE-TIME → UNTIL is amended to DATE-TIME with time and timezone from DTSTART
150+
* - UNTIL is DATE-TIME, DTSTART is DATE → UNTIL is reduced to its date component
151+
*
152+
* @see at.bitfire.synctools.mapping.calendar.builder.EndTimeBuilder.alignWithDtStart
153+
*/
154+
fun alignUntil(recur: Recur, startDate: Date): Recur {
155+
val until: Date? = recur.until
156+
if (until == null)
157+
return recur
158+
159+
if (until is DateTime) {
160+
// UNTIL is DATE-TIME
161+
if (startDate is DateTime) {
162+
// DTSTART is DATE-TIME
163+
return recur
164+
} else {
165+
// DTSTART is DATE → only take date part for UNTIL
166+
val untilDate = until.toLocalDate()
167+
return Recur.Builder(recur)
168+
.until(untilDate.toIcal4jDate())
169+
.build()
170+
}
171+
} else {
172+
// UNTIL is DATE
173+
if (startDate is DateTime) {
174+
// DTSTART is DATE-TIME → amend UNTIL to UTC DATE-TIME
175+
val untilDate = until.toLocalDate()
176+
val startTime = startDate.toZonedDateTime()
177+
val untilDateWithTime = ZonedDateTime.of(untilDate, startTime.toLocalTime(), startTime.zone)
178+
return Recur.Builder(recur)
179+
.until(untilDateWithTime.toIcal4jDateTime(tzRegistry).apply {
180+
isUtc = true
181+
})
182+
.build()
183+
} else {
184+
// DTSTART is DATE
185+
return recur
186+
}
187+
}
188+
}
189+
112190
}

lib/src/main/kotlin/at/bitfire/synctools/util/AndroidTimeUtils.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,9 @@ object AndroidTimeUtils {
144144
* Concatenates, if necessary, multiple RDATE/EXDATE lists and converts them to
145145
* a formatted string which Android calendar provider can process.
146146
*
147-
* Android expects this format: "[TZID;]date1,date2,date3" where date is "yyyymmddThhmmss" (when
148-
* TZID is given) or "yyyymmddThhmmssZ". We don't use the TZID format here because then we're limited
147+
* Android [expects this format](https://android.googlesource.com/platform/frameworks/opt/calendar/+/68b3632330e7a9a4f9813b7eb671dbfd78c25bcd/src/com/android/calendarcommon2/RecurrenceSet.java#138):
148+
* `[TZID;]date1,date2,date3` where date is `yyyymmddThhmmss` (when
149+
* TZID is given) or `yyyymmddThhmmssZ`. We don't use the TZID format here because then we're limited
149150
* to one time-zone, while an iCalendar may contain multiple EXDATE/RDATE lines with different time zones.
150151
*
151152
* This method converts the values to the type of [dtStart], if necessary:
@@ -231,7 +232,7 @@ object AndroidTimeUtils {
231232
* constructed from these values.
232233
*
233234
* @param dbStr formatted string from Android calendar provider (RDATE/EXDATE field)
234-
* expected format: "[TZID;]date1,date2,date3" where date is "yyyymmddThhmmss[Z]"
235+
* expected format: `[TZID;]date1,date2,date3` where date is `yyyymmddThhmmss[Z]`
235236
* @param tzRegistry time zone registry
236237
* @param allDay true: list will contain DATE values; false: list will contain DATE_TIME values
237238
* @param exclude this time stamp won't be added to the [DateListProperty]

0 commit comments

Comments
 (0)