Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 80 additions & 32 deletions lib/src/main/kotlin/at/bitfire/ical4android/TaskReader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,51 @@

package at.bitfire.ical4android

import at.bitfire.ical4android.ICalendar.Companion.fromReader
import at.bitfire.ical4android.util.DateUtils
import at.bitfire.synctools.exception.InvalidICalendarException
import at.bitfire.synctools.icalendar.Css3Color
import at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDate
import at.bitfire.synctools.icalendar.ICalendarParser
import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.toTimestamp
import net.fortuna.ical4j.model.Component
import net.fortuna.ical4j.model.TemporalAdapter
import net.fortuna.ical4j.model.component.VToDo
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.Comment
import net.fortuna.ical4j.model.property.Completed
import net.fortuna.ical4j.model.property.Created
import net.fortuna.ical4j.model.property.Description
import net.fortuna.ical4j.model.property.DtStamp
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.ExDate
import net.fortuna.ical4j.model.property.Geo
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.PercentComplete
import net.fortuna.ical4j.model.property.Priority
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.RelatedTo
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.Uid
import net.fortuna.ical4j.model.property.Url
import java.io.IOException
import java.io.Reader
import java.util.LinkedList
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.ZonedDateTime
import java.time.temporal.Temporal
import java.util.logging.Logger

/**
Expand All @@ -24,28 +62,26 @@ class TaskReader {
get() = Logger.getLogger(TaskReader::class.java.name)

/**
* Parses an iCalendar resource, applies [at.bitfire.synctools.icalendar.validation.ICalPreprocessor] to increase compatibility
* and extracts the VTODOs.
* Parses an iCalendar resource and extracts the VTODOs.
*
* @param reader where the iCalendar is taken from
*
* @return array of filled [Task] data objects (may have size 0)
* @return list of filled [Task] data objects (may have size 0)
*
* @throws InvalidICalendarException when the iCalendar can't be parsed
* @throws IOException on I/O errors
*/
fun readTasks(reader: Reader): List<Task> {
val ical = fromReader(reader)
val ical = ICalendarParser().parse(reader)
val vToDos = ical.getComponents<VToDo>(Component.VTODO)
return vToDos.mapTo(LinkedList()) { this.fromVToDo(it) }
return vToDos.map { fromVToDo(it) }
}

private fun fromVToDo(todo: VToDo): Task {
TODO("ical4j 4.x")
/*val t = Task()
val t = Task()

if (todo.uid != null)
t.uid = todo.uid.value
if (todo.uid.isPresent)
t.uid = todo.uid.get().value
else {
logger.warning("Received VTODO without UID, generating new one")
t.generateUID()
Expand All @@ -54,11 +90,11 @@ class TaskReader {
// sequence must only be null for locally created, not-yet-synchronized events
t.sequence = 0

for (prop in todo.properties)
for (prop in todo.propertyList.all)
when (prop) {
is Sequence -> t.sequence = prop.sequenceNo
is Created -> t.createdAt = prop.dateTime.time
is LastModified -> t.lastModified = prop.dateTime.time
is Created -> t.createdAt = prop.date.toTimestamp()
is LastModified -> t.lastModified = prop.date.toTimestamp()
is Summary -> t.summary = prop.value
is Location -> t.location = prop.value
is Geo -> t.geoPosition = prop
Expand All @@ -69,17 +105,15 @@ class TaskReader {
is Priority -> t.priority = prop.level
is Clazz -> t.classification = prop
is Status -> t.status = prop
is Due -> { t.due = prop }
is Due<*> -> { t.due = prop }
is Duration -> t.duration = prop
is DtStart -> { t.dtStart = prop }
is DtStart<*> -> { t.dtStart = prop }
is Completed -> { t.completedAt = prop }
is PercentComplete -> t.percentComplete = prop.percentage
is RRule -> t.rRule = prop
is RDate -> t.rDates += prop
is ExDate -> t.exDates += prop
is Categories ->
for (category in prop.categories)
t.categories += category
is RRule<*> -> t.rRule = prop
is RDate<*> -> t.rDates += prop
is ExDate<*> -> t.exDates += prop
is Categories -> t.categories.addAll(prop.categories.texts)
is Comment -> t.comment = prop.value
is RelatedTo -> t.relatedTo.add(prop)
is Uid, is ProdId, is DtStamp -> { /* don't save these as unknown properties */ }
Expand All @@ -89,20 +123,21 @@ class TaskReader {
t.alarms.addAll(todo.alarms)

// There seem to be many invalid tasks out there because of some defect clients, do some validation.
val dtStart = t.dtStart
val due = t.due
val startDate = t.dtStart?.normalizedDate()
val dueDate = t.due?.normalizedDate()

if (dtStart != null && due != null) {
if (DateUtils.isDate(dtStart) && DateUtils.isDateTime(due)) {
if (startDate != null && dueDate != null) {
if (startDate is LocalDate && DateUtils.isDateTime(dueDate)) {
logger.warning("DTSTART is DATE but DUE is DATE-TIME, rewriting DTSTART to DATE-TIME")
t.dtStart = DtStart(DateTime(dtStart.value, due.timeZone))
} else if (DateUtils.isDateTime(dtStart) && DateUtils.isDate(due)) {
t.dtStart = DtStart(startDate.toDateTime(dueDate))
} else if (DateUtils.isDateTime(startDate) && dueDate is LocalDate) {
logger.warning("DTSTART is DATE-TIME but DUE is DATE, rewriting DUE to DATE-TIME")
t.due = Due(DateTime(due.value, dtStart.timeZone))
t.due = Due(dueDate.toDateTime(startDate))
}


if (due.date <= dtStart.date) {
val newStartDate = t.dtStart!!.date
val newDueDate = t.due!!.date
if (TemporalAdapter.isAfter(newStartDate, newDueDate)) {
logger.warning("Found invalid DUE <= DTSTART; dropping DTSTART")
t.dtStart = null
}
Expand All @@ -113,7 +148,20 @@ class TaskReader {
t.duration = null
}

return t*/
return t
}

/**
* Converts this [LocalDate] to a date-time [Temporal] of the same type as `referenceDateTime`.
*/
private fun LocalDate.toDateTime(referenceDateTime: Temporal): Temporal {
return when (referenceDateTime) {
is LocalDateTime -> atStartOfDay()
is ZonedDateTime -> atStartOfDay(referenceDateTime.zone)
is OffsetDateTime -> OffsetDateTime.of(atStartOfDay(), referenceDateTime.offset)
is Instant -> atStartOfDay(ZoneOffset.UTC).toInstant()
else -> error("Unsupported Temporal type: ${this::class.qualifiedName}")
}
}

}
135 changes: 89 additions & 46 deletions lib/src/main/kotlin/at/bitfire/ical4android/TaskWriter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,43 @@

package at.bitfire.ical4android

import at.bitfire.ical4android.ICalendar.Companion.softValidate
import at.bitfire.ical4android.ICalendar.Companion.withUserAgents
import at.bitfire.synctools.icalendar.Css3Color
import at.bitfire.synctools.icalendar.VTimeZoneMinifier
import at.bitfire.synctools.icalendar.plusAssign
import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.toTimestamp
import net.fortuna.ical4j.data.CalendarOutputter
import net.fortuna.ical4j.model.Calendar
import net.fortuna.ical4j.model.Parameter
import net.fortuna.ical4j.model.TextList
import net.fortuna.ical4j.model.TimeZoneRegistryFactory
import net.fortuna.ical4j.model.component.VToDo
import net.fortuna.ical4j.model.parameter.TzId
import net.fortuna.ical4j.model.property.Categories
import net.fortuna.ical4j.model.property.Color
import net.fortuna.ical4j.model.property.Comment
import net.fortuna.ical4j.model.property.Created
import net.fortuna.ical4j.model.property.DateProperty
import net.fortuna.ical4j.model.property.Description
import net.fortuna.ical4j.model.property.LastModified
import net.fortuna.ical4j.model.property.Location
import net.fortuna.ical4j.model.property.PercentComplete
import net.fortuna.ical4j.model.property.Priority
import net.fortuna.ical4j.model.property.ProdId
import net.fortuna.ical4j.model.property.RelatedTo
import net.fortuna.ical4j.model.property.Sequence
import net.fortuna.ical4j.model.property.Summary
import net.fortuna.ical4j.model.property.Uid
import net.fortuna.ical4j.model.property.Url
import net.fortuna.ical4j.model.property.immutable.ImmutableVersion
import java.io.Writer
import java.net.URI
import java.net.URISyntaxException
import java.time.Instant
import java.util.logging.Level
import java.util.logging.Logger
import kotlin.jvm.optionals.getOrNull

/**
* Writes a [Task] data class to a stream that contains an iCalendar
Expand All @@ -23,6 +57,7 @@ class TaskWriter(
private val logger
get() = Logger.getLogger(TaskWriter::class.java.name)

private val tzRegistry by lazy { TimeZoneRegistryFactory.getInstance().createRegistry() }

/**
* Generates an iCalendar from the provided Task.
Expand All @@ -31,85 +66,93 @@ class TaskWriter(
* @param to stream that the iCalendar is written to
*/
fun write(task: Task, to: Writer): Unit = with(task) {
TODO()
/*val ical = Calendar()
ical.properties += Version.VERSION_2_0
ical.properties += prodId.withUserAgents(userAgents)
val ical = Calendar()
ical += ImmutableVersion.VERSION_2_0
ical += prodId.withUserAgents(userAgents)

val vTodo = VToDo(true /* generates DTSTAMP */)
ical.components += vTodo
val props = vTodo.properties
ical += vTodo

uid?.let { props += Uid(uid) }
uid?.let { vTodo += Uid(uid) }
sequence?.let {
if (it != 0)
props += Sequence(it)
vTodo += Sequence(it)
}

createdAt?.let { props += Created(DateTime(it)) }
lastModified?.let { props += LastModified(DateTime(it)) }
createdAt?.let { vTodo += Created(Instant.ofEpochMilli(it)) }
lastModified?.let { vTodo += LastModified(Instant.ofEpochMilli(it)) }

summary?.let { props += Summary(it) }
location?.let { props += Location(it) }
geoPosition?.let { props += it }
description?.let { props += Description(it) }
color?.let { props += Color(null, Css3Color.nearestMatch(it).name) }
summary?.let { vTodo += Summary(it) }
location?.let { vTodo += Location(it) }
geoPosition?.let { vTodo += it }
description?.let { vTodo += Description(it) }
color?.let { vTodo += Color(null, Css3Color.nearestMatch(it).name) }
url?.let {
try {
props += Url(URI(it))
vTodo += Url(URI(it))
} catch (e: URISyntaxException) {
logger.log(Level.WARNING, "Ignoring invalid task URL: $url", e)
}
}
organizer?.let { props += it }
organizer?.let { vTodo += it }

if (priority != Priority.UNDEFINED.level)
props += Priority(priority)
classification?.let { props += it }
status?.let { props += it }
if (priority != Priority.VALUE_UNDEFINED)
vTodo += Priority(priority)
classification?.let { vTodo += it }
status?.let { vTodo += it }

rRule?.let { props += it }
rDates.forEach { props += it }
exDates.forEach { props += it }
rRule?.let { vTodo += it }
rDates.forEach { vTodo += it }
exDates.forEach { vTodo += it }

if (categories.isNotEmpty())
props += Categories(TextList(categories.toTypedArray()))
comment?.let { props += Comment(it) }
props.addAll(relatedTo)
props.addAll(unknownProperties)
vTodo += Categories(TextList(categories))
comment?.let { vTodo += Comment(it) }
vTodo.addAll<VToDo>(relatedTo as Collection<RelatedTo>)
vTodo.addAll<VToDo>(unknownProperties)

// remember used time zones
val usedTimeZones = HashSet<TimeZone>()
due?.let {
props += it
it.timeZone?.let(usedTimeZones::add)
val usedTimeZones = mutableSetOf<String>()
due?.let { due ->
vTodo += due
due.getTzidOrNull()?.let(usedTimeZones::add)
}
duration?.let(props::add)
dtStart?.let {
props += it
it.timeZone?.let(usedTimeZones::add)
duration?.let { vTodo += it }
dtStart?.let { dtStart ->
vTodo += dtStart
dtStart.getTzidOrNull()?.let(usedTimeZones::add)
}
completedAt?.let {
props += it
it.timeZone?.let(usedTimeZones::add)
completedAt?.let { completedAt ->
vTodo += completedAt
completedAt.getTzidOrNull()?.let(usedTimeZones::add)
}
percentComplete?.let { props += PercentComplete(it) }
percentComplete?.let { vTodo += PercentComplete(it) }

if (alarms.isNotEmpty())
vTodo.components.addAll(alarms)
for (alarm in alarms) {
vTodo.add<VToDo>(alarm)
}

// determine earliest referenced date
val earliest = arrayOf(
dtStart?.date,
due?.date,
completedAt?.date
).filterNotNull().minOrNull()
).filterNotNull().minByOrNull { it.toTimestamp() }


val timeZoneMinifier = VTimeZoneMinifier()
// add VTIMEZONE components
for (tz in usedTimeZones)
ical.components += minifyVTimeZone(tz.vTimeZone, earliest)
for (tz in usedTimeZones) {
val vTimeZone = tzRegistry.getTimeZone(tz).vTimeZone
val minifiedVTimeZone = timeZoneMinifier.minify(vTimeZone, earliest)
ical += minifiedVTimeZone
}

softValidate(ical)
CalendarOutputter(false).output(ical, to)*/
CalendarOutputter(false).output(ical, to)
}

private fun DateProperty<*>.getTzidOrNull(): String? {
return getParameter<TzId>(Parameter.TZID).getOrNull()?.value
}
}
Loading