Skip to content

Commit 183330d

Browse files
committed
Extract task writing/generation from data class
1 parent 20ae9be commit 183330d

File tree

5 files changed

+201
-173
lines changed

5 files changed

+201
-173
lines changed

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

Lines changed: 3 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -8,48 +8,22 @@ package at.bitfire.ical4android
88

99
import androidx.annotation.IntRange
1010
import at.bitfire.ical4android.util.DateUtils
11-
import at.bitfire.synctools.icalendar.Css3Color
12-
import net.fortuna.ical4j.data.CalendarOutputter
13-
import net.fortuna.ical4j.model.Calendar
14-
import net.fortuna.ical4j.model.DateTime
1511
import net.fortuna.ical4j.model.Property
16-
import net.fortuna.ical4j.model.TextList
17-
import net.fortuna.ical4j.model.TimeZone
1812
import net.fortuna.ical4j.model.component.VAlarm
19-
import net.fortuna.ical4j.model.component.VToDo
20-
import net.fortuna.ical4j.model.property.Categories
2113
import net.fortuna.ical4j.model.property.Clazz
22-
import net.fortuna.ical4j.model.property.Color
23-
import net.fortuna.ical4j.model.property.Comment
2414
import net.fortuna.ical4j.model.property.Completed
25-
import net.fortuna.ical4j.model.property.Created
26-
import net.fortuna.ical4j.model.property.Description
2715
import net.fortuna.ical4j.model.property.DtStart
2816
import net.fortuna.ical4j.model.property.Due
2917
import net.fortuna.ical4j.model.property.Duration
3018
import net.fortuna.ical4j.model.property.ExDate
3119
import net.fortuna.ical4j.model.property.Geo
32-
import net.fortuna.ical4j.model.property.LastModified
33-
import net.fortuna.ical4j.model.property.Location
3420
import net.fortuna.ical4j.model.property.Organizer
35-
import net.fortuna.ical4j.model.property.PercentComplete
3621
import net.fortuna.ical4j.model.property.Priority
37-
import net.fortuna.ical4j.model.property.ProdId
3822
import net.fortuna.ical4j.model.property.RDate
3923
import net.fortuna.ical4j.model.property.RRule
4024
import net.fortuna.ical4j.model.property.RelatedTo
41-
import net.fortuna.ical4j.model.property.Sequence
4225
import net.fortuna.ical4j.model.property.Status
43-
import net.fortuna.ical4j.model.property.Summary
44-
import net.fortuna.ical4j.model.property.Uid
45-
import net.fortuna.ical4j.model.property.Url
46-
import net.fortuna.ical4j.model.property.Version
47-
import java.io.OutputStream
48-
import java.net.URI
49-
import java.net.URISyntaxException
5026
import java.util.LinkedList
51-
import java.util.logging.Level
52-
import java.util.logging.Logger
5327

5428
/**
5529
* Data class representing a task
@@ -95,106 +69,10 @@ data class Task(
9569
val alarms: LinkedList<VAlarm> = LinkedList(),
9670
) : ICalendar() {
9771

98-
companion object {
99-
100-
private val logger
101-
get() = Logger.getLogger(Task::class.java.name)
102-
103-
}
104-
105-
106-
/**
107-
* Generates an iCalendar from the Task.
108-
*
109-
* @param os stream that the iCalendar is written to
110-
* @param prodId `PRODID` that identifies the app
111-
*/
112-
fun write(os: OutputStream, prodId: ProdId) {
113-
val ical = Calendar()
114-
ical.properties += Version.VERSION_2_0
115-
ical.properties += prodId.withUserAgents(userAgents)
116-
117-
val vTodo = VToDo(true /* generates DTSTAMP */)
118-
ical.components += vTodo
119-
val props = vTodo.properties
120-
121-
uid?.let { props += Uid(uid) }
122-
sequence?.let {
123-
if (it != 0)
124-
props += Sequence(it)
125-
}
126-
127-
createdAt?.let { props += Created(DateTime(it)) }
128-
lastModified?.let { props += LastModified(DateTime(it)) }
129-
130-
summary?.let { props += Summary(it) }
131-
location?.let { props += Location(it) }
132-
geoPosition?.let { props += it }
133-
description?.let { props += Description(it) }
134-
color?.let { props += Color(null, Css3Color.nearestMatch(it).name) }
135-
url?.let {
136-
try {
137-
props += Url(URI(it))
138-
} catch (e: URISyntaxException) {
139-
logger.log(Level.WARNING, "Ignoring invalid task URL: $url", e)
140-
}
141-
}
142-
organizer?.let { props += it }
143-
144-
if (priority != Priority.UNDEFINED.level)
145-
props += Priority(priority)
146-
classification?.let { props += it }
147-
status?.let { props += it }
148-
149-
rRule?.let { props += it }
150-
rDates.forEach { props += it }
151-
exDates.forEach { props += it }
152-
153-
if (categories.isNotEmpty())
154-
props += Categories(TextList(categories.toTypedArray()))
155-
comment?.let { props += Comment(it) }
156-
props.addAll(relatedTo)
157-
props.addAll(unknownProperties)
158-
159-
// remember used time zones
160-
val usedTimeZones = HashSet<TimeZone>()
161-
due?.let {
162-
props += it
163-
it.timeZone?.let(usedTimeZones::add)
164-
}
165-
duration?.let(props::add)
166-
dtStart?.let {
167-
props += it
168-
it.timeZone?.let(usedTimeZones::add)
169-
}
170-
completedAt?.let {
171-
props += it
172-
it.timeZone?.let(usedTimeZones::add)
173-
}
174-
percentComplete?.let { props += PercentComplete(it) }
175-
176-
if (alarms.isNotEmpty())
177-
vTodo.components.addAll(alarms)
178-
179-
// determine earliest referenced date
180-
val earliest = arrayOf(
181-
dtStart?.date,
182-
due?.date,
183-
completedAt?.date
184-
).filterNotNull().minOrNull()
185-
// add VTIMEZONE components
186-
for (tz in usedTimeZones)
187-
ical.components += minifyVTimeZone(tz.vTimeZone, earliest)
188-
189-
softValidate(ical)
190-
CalendarOutputter(false).output(ical, os)
191-
}
192-
193-
19472
fun isAllDay(): Boolean {
195-
return dtStart?.let { DateUtils.isDate(it) } ?:
196-
due?.let { DateUtils.isDate(it) } ?:
197-
true
73+
return dtStart?.let { DateUtils.isDate(it) }
74+
?: due?.let { DateUtils.isDate(it) }
75+
?: true
19876
}
19977

20078
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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.ical4android
8+
9+
import at.bitfire.ical4android.ICalendar.Companion.minifyVTimeZone
10+
import at.bitfire.ical4android.ICalendar.Companion.softValidate
11+
import at.bitfire.ical4android.ICalendar.Companion.withUserAgents
12+
import at.bitfire.synctools.icalendar.Css3Color
13+
import net.fortuna.ical4j.data.CalendarOutputter
14+
import net.fortuna.ical4j.model.Calendar
15+
import net.fortuna.ical4j.model.DateTime
16+
import net.fortuna.ical4j.model.TextList
17+
import net.fortuna.ical4j.model.TimeZone
18+
import net.fortuna.ical4j.model.component.VToDo
19+
import net.fortuna.ical4j.model.property.Categories
20+
import net.fortuna.ical4j.model.property.Color
21+
import net.fortuna.ical4j.model.property.Comment
22+
import net.fortuna.ical4j.model.property.Created
23+
import net.fortuna.ical4j.model.property.Description
24+
import net.fortuna.ical4j.model.property.LastModified
25+
import net.fortuna.ical4j.model.property.Location
26+
import net.fortuna.ical4j.model.property.PercentComplete
27+
import net.fortuna.ical4j.model.property.Priority
28+
import net.fortuna.ical4j.model.property.ProdId
29+
import net.fortuna.ical4j.model.property.Sequence
30+
import net.fortuna.ical4j.model.property.Summary
31+
import net.fortuna.ical4j.model.property.Uid
32+
import net.fortuna.ical4j.model.property.Url
33+
import net.fortuna.ical4j.model.property.Version
34+
import java.io.OutputStream
35+
import java.net.URI
36+
import java.net.URISyntaxException
37+
import java.util.logging.Level
38+
import java.util.logging.Logger
39+
40+
/**
41+
* Writes a [Task] data class to a stream that contains an iCalendar
42+
* (VCALENDAR with VTODOs and optional VTIMEZONEs).
43+
*
44+
* @param prodId PRODID to use in iCalendar, which identifies DAVx⁵
45+
*/
46+
class TaskWriter(
47+
private val prodId: ProdId
48+
) {
49+
50+
private val logger
51+
get() = Logger.getLogger(TaskWriter::class.java.name)
52+
53+
54+
/**
55+
* Generates an iCalendar from the provided Task.
56+
*
57+
* @param os stream that the iCalendar is written to
58+
*/
59+
fun write(task: Task, os: OutputStream) = task.run {
60+
val ical = Calendar()
61+
ical.properties += Version.VERSION_2_0
62+
ical.properties += prodId.withUserAgents(userAgents)
63+
64+
val vTodo = VToDo(true /* generates DTSTAMP */)
65+
ical.components += vTodo
66+
val props = vTodo.properties
67+
68+
uid?.let { props += Uid(uid) }
69+
sequence?.let {
70+
if (it != 0)
71+
props += Sequence(it)
72+
}
73+
74+
createdAt?.let { props += Created(DateTime(it)) }
75+
lastModified?.let { props += LastModified(DateTime(it)) }
76+
77+
summary?.let { props += Summary(it) }
78+
location?.let { props += Location(it) }
79+
geoPosition?.let { props += it }
80+
description?.let { props += Description(it) }
81+
color?.let { props += Color(null, Css3Color.nearestMatch(it).name) }
82+
url?.let {
83+
try {
84+
props += Url(URI(it))
85+
} catch (e: URISyntaxException) {
86+
logger.log(Level.WARNING, "Ignoring invalid task URL: $url", e)
87+
}
88+
}
89+
organizer?.let { props += it }
90+
91+
if (priority != Priority.UNDEFINED.level)
92+
props += Priority(priority)
93+
classification?.let { props += it }
94+
status?.let { props += it }
95+
96+
rRule?.let { props += it }
97+
rDates.forEach { props += it }
98+
exDates.forEach { props += it }
99+
100+
if (categories.isNotEmpty())
101+
props += Categories(TextList(categories.toTypedArray()))
102+
comment?.let { props += Comment(it) }
103+
props.addAll(relatedTo)
104+
props.addAll(unknownProperties)
105+
106+
// remember used time zones
107+
val usedTimeZones = HashSet<TimeZone>()
108+
due?.let {
109+
props += it
110+
it.timeZone?.let(usedTimeZones::add)
111+
}
112+
duration?.let(props::add)
113+
dtStart?.let {
114+
props += it
115+
it.timeZone?.let(usedTimeZones::add)
116+
}
117+
completedAt?.let {
118+
props += it
119+
it.timeZone?.let(usedTimeZones::add)
120+
}
121+
percentComplete?.let { props += PercentComplete(it) }
122+
123+
if (alarms.isNotEmpty())
124+
vTodo.components.addAll(alarms)
125+
126+
// determine earliest referenced date
127+
val earliest = arrayOf(
128+
dtStart?.date,
129+
due?.date,
130+
completedAt?.date
131+
).filterNotNull().minOrNull()
132+
// add VTIMEZONE components
133+
for (tz in usedTimeZones)
134+
ical.components += minifyVTimeZone(tz.vTimeZone, earliest)
135+
136+
softValidate(ical)
137+
CalendarOutputter(false).output(ical, os)
138+
}
139+
140+
}

lib/src/test/kotlin/at/bitfire/ical4android/TaskReaderTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ class TaskReaderTest {
211211

212212
private fun regenerate(t: Task): Task {
213213
val os = ByteArrayOutputStream()
214-
t.write(os, testProdId)
214+
TaskWriter(testProdId).write(t, os)
215215
return TaskReader().tasksFromReader(InputStreamReader(ByteArrayInputStream(os.toByteArray()), Charsets.UTF_8)).first()
216216
}
217217
}

lib/src/test/kotlin/at/bitfire/ical4android/TaskTest.kt

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,61 +8,14 @@ package at.bitfire.ical4android
88

99
import net.fortuna.ical4j.model.Date
1010
import net.fortuna.ical4j.model.DateTime
11-
import net.fortuna.ical4j.model.TimeZone
12-
import net.fortuna.ical4j.model.TimeZoneRegistryFactory
13-
import net.fortuna.ical4j.model.component.VAlarm
14-
import net.fortuna.ical4j.model.property.Action
1511
import net.fortuna.ical4j.model.property.DtStart
1612
import net.fortuna.ical4j.model.property.Due
17-
import net.fortuna.ical4j.model.property.ProdId
1813
import org.junit.Assert.assertFalse
1914
import org.junit.Assert.assertTrue
2015
import org.junit.Test
21-
import java.io.ByteArrayOutputStream
22-
import java.time.Duration
2316

2417
class TaskTest {
2518

26-
val testProdId = ProdId(javaClass.name)
27-
28-
val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry()!!
29-
val tzBerlin: TimeZone = tzRegistry.getTimeZone("Europe/Berlin")!!
30-
31-
32-
/* generating */
33-
34-
@Test
35-
fun testWrite() {
36-
val t = Task()
37-
t.uid = "SAMPLEUID"
38-
t.dtStart = DtStart("20190101T100000", tzBerlin)
39-
40-
val alarm = VAlarm(Duration.ofHours(-1) /*Dur(0, -1, 0, 0)*/)
41-
alarm.properties += Action.AUDIO
42-
t.alarms += alarm
43-
44-
val os = ByteArrayOutputStream()
45-
t.write(os, testProdId)
46-
val raw = os.toString(Charsets.UTF_8.name())
47-
48-
assertTrue(raw.contains("PRODID:${testProdId.value}"))
49-
assertTrue(raw.contains("UID:SAMPLEUID"))
50-
assertTrue(raw.contains("DTSTAMP:"))
51-
assertTrue(raw.contains("DTSTART;TZID=Europe/Berlin:20190101T100000"))
52-
assertTrue(
53-
raw.contains(
54-
"BEGIN:VALARM\r\n" +
55-
"TRIGGER:-PT1H\r\n" +
56-
"ACTION:AUDIO\r\n" +
57-
"END:VALARM\r\n"
58-
)
59-
)
60-
assertTrue(raw.contains("BEGIN:VTIMEZONE"))
61-
}
62-
63-
64-
/* other methods */
65-
6619
@Test
6720
fun testAllDay() {
6821
assertTrue(Task().isAllDay())

0 commit comments

Comments
 (0)