Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.

Commit 00b8896

Browse files
authored
[jtx] Add RecurrenceFieldsBuilder (#388)
1 parent 58128cf commit 00b8896

3 files changed

Lines changed: 470 additions & 1 deletion

File tree

lib/src/main/kotlin/at/bitfire/synctools/mapping/jtx/JtxItemBuilder.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import android.content.Entity
1111
import at.bitfire.synctools.icalendar.AssociatedComponents
1212
import at.bitfire.synctools.mapping.jtx.builder.CollectionIdBuilder
1313
import at.bitfire.synctools.mapping.jtx.builder.DescriptionBuilder
14+
import at.bitfire.synctools.mapping.jtx.builder.RecurrenceFieldsBuilder
1415
import at.bitfire.synctools.mapping.jtx.builder.JtxEntityBuilder
1516
import at.bitfire.synctools.mapping.jtx.builder.SyncPropertiesBuilder
1617
import at.bitfire.synctools.storage.jtx.JtxItemAndExceptions
@@ -33,7 +34,8 @@ class JtxItemBuilder(
3334
CollectionIdBuilder(collectionId),
3435
SyncPropertiesBuilder(fileName, eTag, scheduleTag, flags),
3536

36-
DescriptionBuilder()
37+
DescriptionBuilder(),
38+
RecurrenceFieldsBuilder()
3739
)
3840

3941
fun build(component: AssociatedComponents<CalendarComponent>): JtxItemAndExceptions {
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* This file is part of bitfireAT/synctools which is released under GPLv3.
3+
* Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details.
4+
* SPDX-License-Identifier: GPL-3.0-or-later
5+
*/
6+
7+
package at.bitfire.synctools.mapping.jtx.builder
8+
9+
import android.content.Entity
10+
import at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDate
11+
import at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDates
12+
import at.bitfire.synctools.icalendar.recurrenceId
13+
import at.bitfire.synctools.util.AndroidTimeUtils.toTimestamp
14+
import at.techbee.jtx.JtxContract
15+
import net.fortuna.ical4j.model.Property
16+
import net.fortuna.ical4j.model.component.CalendarComponent
17+
import net.fortuna.ical4j.model.property.DateListProperty
18+
import net.fortuna.ical4j.model.property.RRule
19+
import java.time.Instant
20+
import java.time.LocalDate
21+
import java.time.LocalDateTime
22+
import java.time.ZoneOffset
23+
import java.time.ZonedDateTime
24+
import java.time.temporal.Temporal
25+
import java.util.logging.Logger
26+
27+
class RecurrenceFieldsBuilder : JtxEntityBuilder {
28+
29+
private val logger
30+
get() = Logger.getLogger(javaClass.name)
31+
32+
override fun build(from: CalendarComponent, main: CalendarComponent, to: Entity) {
33+
if (from === main) {
34+
buildMainItem(main, to)
35+
} else {
36+
buildExceptionItem(from, to)
37+
}
38+
}
39+
40+
private fun buildMainItem(main: CalendarComponent, to: Entity) {
41+
to.entityValues.putNull(JtxContract.JtxICalObject.RECURID)
42+
to.entityValues.putNull(JtxContract.JtxICalObject.RECURID_TIMEZONE)
43+
44+
buildRRule(main, to)
45+
buildRDate(main, to)
46+
buildExDate(main, to)
47+
}
48+
49+
private fun buildRRule(main: CalendarComponent, to: Entity) {
50+
val rRules = main.getProperties<RRule<*>>(Property.RRULE)
51+
if (rRules.isEmpty()) {
52+
to.entityValues.putNull(JtxContract.JtxICalObject.RRULE)
53+
return
54+
}
55+
56+
// Note: All but the last RRULE property are ignored.
57+
val rrule = rRules.last().value
58+
to.entityValues.put(JtxContract.JtxICalObject.RRULE, rrule)
59+
}
60+
61+
private fun buildRDate(main: CalendarComponent, to: Entity) {
62+
// Note: RDATE properties with a PERIOD value are currently not supported and ignored.
63+
buildDateList(main, to, Property.RDATE, JtxContract.JtxICalObject.RDATE)
64+
}
65+
66+
private fun buildExDate(main: CalendarComponent, to: Entity) {
67+
buildDateList(main, to, Property.EXDATE, JtxContract.JtxICalObject.EXDATE)
68+
}
69+
70+
private fun buildDateList(
71+
main: CalendarComponent,
72+
to: Entity,
73+
propertyName: String,
74+
columnName: String
75+
) {
76+
val dateListProperties = main.getProperties<DateListProperty<*>>(propertyName)
77+
if (dateListProperties.isEmpty()) {
78+
to.entityValues.putNull(columnName)
79+
return
80+
}
81+
82+
val timestampListString = dateListProperties
83+
.flatMap { it.normalizedDates() }
84+
.map { it.toTimestamp() }
85+
.joinToString(separator = ",")
86+
.takeIf { it.isNotEmpty() }
87+
88+
if (timestampListString == null) {
89+
to.entityValues.putNull(columnName)
90+
} else {
91+
to.entityValues.put(columnName, timestampListString)
92+
}
93+
}
94+
95+
private fun buildExceptionItem(exception: CalendarComponent, to: Entity) {
96+
buildRecurrenceId(exception, to)
97+
98+
to.entityValues.putNull(JtxContract.JtxICalObject.RRULE)
99+
to.entityValues.putNull(JtxContract.JtxICalObject.RDATE)
100+
to.entityValues.putNull(JtxContract.JtxICalObject.EXDATE)
101+
}
102+
103+
private fun buildRecurrenceId(exception: CalendarComponent, to: Entity) {
104+
val recurrenceId = exception.recurrenceId
105+
if (recurrenceId == null) {
106+
to.entityValues.putNull(JtxContract.JtxICalObject.RECURID)
107+
to.entityValues.putNull(JtxContract.JtxICalObject.RECURID_TIMEZONE)
108+
return
109+
}
110+
111+
to.entityValues.put(JtxContract.JtxICalObject.RECURID, recurrenceId.value)
112+
val timeZoneId = recurrenceId.normalizedDate().getTimeZoneId()
113+
to.entityValues.put(JtxContract.JtxICalObject.RECURID_TIMEZONE, timeZoneId)
114+
}
115+
116+
private fun Temporal.getTimeZoneId(): String? = when (this) {
117+
is ZonedDateTime -> {
118+
this.zone.id
119+
}
120+
is Instant -> {
121+
ZoneOffset.UTC.id
122+
}
123+
is LocalDateTime -> {
124+
// Timezone unknown => floating time
125+
null
126+
}
127+
is LocalDate -> {
128+
// Without time, it is considered all-day
129+
JtxContract.JtxICalObject.TZ_ALLDAY
130+
}
131+
else -> {
132+
logger.warning("Ignoring unsupported temporal type: ${this::class}")
133+
null
134+
}
135+
}
136+
}

0 commit comments

Comments
 (0)