Event.kt 10.4 KB
Newer Older
Ricki Hirner's avatar
Ricki Hirner committed
1
/*
Ricki Hirner's avatar
Ricki Hirner committed
2 3 4 5 6
 * Copyright © Ricki Hirner (bitfire web engineering).
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl.html
Ricki Hirner's avatar
Ricki Hirner committed
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
 */

package at.bitfire.ical4android

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.Property
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.property.*
import java.io.IOException
import java.io.OutputStream
22
import java.io.Reader
Ricki Hirner's avatar
Ricki Hirner committed
23 24
import java.util.*

Ricki Hirner's avatar
Ricki Hirner committed
25
class Event: ICalendar() {
Ricki Hirner's avatar
Ricki Hirner committed
26 27 28 29 30 31 32

    // uid and sequence are inherited from iCalendar
    var recurrenceId: RecurrenceId? = null

    var summary: String? = null
    var location: String? = null
    var description: String? = null
Ricki Hirner's avatar
Ricki Hirner committed
33
    var color: Css3Color? = null
Ricki Hirner's avatar
Ricki Hirner committed
34 35 36 37 38 39 40 41 42 43 44 45

    var dtStart: DtStart? = null
    var dtEnd: DtEnd? = null

    var duration: Duration? = null
    var rRule: RRule? = null
    var exRule: ExRule? = null
    val rDates = LinkedList<RDate>()
    val exDates = LinkedList<ExDate>()

    val exceptions = LinkedList<Event>()

46
    var classification: Clazz? = null
Ricki Hirner's avatar
Ricki Hirner committed
47 48 49 50 51 52 53 54 55 56 57 58 59 60
    var status: Status? = null

    var opaque = true

    var organizer: Organizer? = null
    val attendees = LinkedList<Attendee>()

    val alarms = LinkedList<VAlarm>()

    var lastModified: LastModified? = null

    val unknownProperties = LinkedList<Property>()

    companion object {
Ricki Hirner's avatar
Ricki Hirner committed
61
        const val CALENDAR_NAME = "X-WR-CALNAME"
Ricki Hirner's avatar
Ricki Hirner committed
62 63 64 65

        /**
         * Parses an InputStream that contains iCalendar VEVENTs.
         *
66
         * @param reader        reader for the input stream containing the VEVENTs (pay attention to the charset)
Ricki Hirner's avatar
Ricki Hirner committed
67 68 69 70 71
         * @param properties    map of properties, will be filled with CALENDAR_* values, if applicable (may be null)
         * @return              array of filled Event data objects (may have size 0) – doesn't return null
         * @throws IOException on I/O errors
         * @throws InvalidCalendarException on parsing exceptions
         */
72 73
        fun fromReader(reader: Reader, properties: MutableMap<String, String>? = null): List<Event> {
            Constants.log.fine("Parsing iCalendar stream")
Ricki Hirner's avatar
Ricki Hirner committed
74 75

            // parse stream
76
            val ical: Calendar
Ricki Hirner's avatar
Ricki Hirner committed
77
            try {
78
                ical = calendarBuilder().build(reader)
79
            } catch(e: ParserException) {
80 81 82
                throw InvalidCalendarException("Couldn't parse iCalendar object", e)
            } catch(e: IllegalArgumentException) {
                throw InvalidCalendarException("iCalendar object contains invalid value", e)
Ricki Hirner's avatar
Ricki Hirner committed
83 84 85 86
            }

            // fill calendar properties
            properties?.let {
bernhard's avatar
bernhard committed
87
                (ical.getProperty(CALENDAR_NAME) as Property)?.let { calName ->
Ricki Hirner's avatar
Ricki Hirner committed
88 89 90 91 92 93 94 95 96 97 98 99
                    properties[CALENDAR_NAME] = calName.value
                }
            }

            // process VEVENTs
            val vEvents = ical.getComponents<VEvent>(Component.VEVENT)

            // make sure every event has an UID
            for (vEvent in vEvents)
                if (vEvent.uid == null) {
                    val uid = Uid(UUID.randomUUID().toString())
                    Constants.log.warning("Found VEVENT without UID, using a random one: ${uid.value}")
100
                    vEvent.properties += uid
Ricki Hirner's avatar
Ricki Hirner committed
101 102
                }

103 104
            Constants.log.fine("Assigning exceptions to main events")
            val mainEvents = mutableMapOf<String /* UID */,VEvent>()
Ricki Hirner's avatar
Ricki Hirner committed
105 106 107 108 109 110 111
            val exceptions = mutableMapOf<String /* UID */,MutableMap<String /* RECURRENCE-ID */,VEvent>>()

            for (vEvent in vEvents) {
                val uid = vEvent.uid.value
                val sequence = vEvent.sequence?.sequenceNo ?: 0

                if (vEvent.recurrenceId == null) {
112
                    // main event (no RECURRENCE-ID)
Ricki Hirner's avatar
Ricki Hirner committed
113 114 115

                    // If there are multiple entries, compare SEQUENCE and use the one with higher SEQUENCE.
                    // If the SEQUENCE is identical, use latest version.
116
                    val event = mainEvents[uid]
Ricki Hirner's avatar
Ricki Hirner committed
117
                    if (event == null || (event.sequence != null && sequence >= event.sequence.sequenceNo))
118
                        mainEvents[uid] = vEvent
Ricki Hirner's avatar
Ricki Hirner committed
119 120 121 122 123 124

                } else {
                    // exception (RECURRENCE-ID)
                    var ex = exceptions[uid]
                    // first index level: UID
                    if (ex == null) {
Ricki Hirner's avatar
Ricki Hirner committed
125
                        ex = mutableMapOf()
Ricki Hirner's avatar
Ricki Hirner committed
126 127 128 129 130 131 132 133 134 135 136
                        exceptions[uid] = ex
                    }
                    // second index level: RECURRENCE-ID
                    val recurrenceID = vEvent.recurrenceId.value
                    val event = ex[recurrenceID]
                    if (event == null || (event.sequence != null && sequence >= event.sequence.sequenceNo))
                        ex[recurrenceID] = vEvent
                }
            }

            val events = mutableListOf<Event>()
137
            for ((uid, vEvent) in mainEvents) {
Ricki Hirner's avatar
Ricki Hirner committed
138 139 140 141
                val event = fromVEvent(vEvent)
                exceptions[uid]?.let { eventExceptions ->
                    event.exceptions.addAll(eventExceptions.map { (_,it) -> fromVEvent(it) })
                }
142 143 144 145

                // make sure that exceptions have at least a SUMMARY
                event.exceptions.forEach { it.summary = it.summary ?: event.summary }

146
                events += event
Ricki Hirner's avatar
Ricki Hirner committed
147 148
            }

149
            return events
Ricki Hirner's avatar
Ricki Hirner committed
150 151
        }

Ricki Hirner's avatar
Ricki Hirner committed
152
        private fun fromVEvent(event: VEvent): Event {
Ricki Hirner's avatar
Ricki Hirner committed
153 154 155 156 157 158 159 160 161 162 163
            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
Ricki Hirner's avatar
Ricki Hirner committed
164 165 166
                    is Summary -> e.summary = prop.value
                    is Location -> e.location = prop.value
                    is Description -> e.description = prop.value
Ricki Hirner's avatar
Ricki Hirner committed
167
                    is Color -> e.color = Css3Color.fromString(prop.value)
Ricki Hirner's avatar
Ricki Hirner committed
168 169 170 171
                    is DtStart -> e.dtStart = prop
                    is DtEnd -> e.dtEnd = prop
                    is Duration -> e.duration = prop
                    is RRule -> e.rRule = prop
172
                    is RDate -> e.rDates += prop
Ricki Hirner's avatar
Ricki Hirner committed
173
                    is ExRule -> e.exRule = prop
174
                    is ExDate -> e.exDates += prop
175
                    is Clazz -> e.classification = prop
Ricki Hirner's avatar
Ricki Hirner committed
176 177 178
                    is Status -> e.status = prop
                    is Transp -> e.opaque = prop == Transp.OPAQUE
                    is Organizer -> e.organizer = prop
179
                    is Attendee -> e.attendees += prop
Ricki Hirner's avatar
Ricki Hirner committed
180
                    is LastModified -> e.lastModified = prop
181
                    is ProdId, is DtStamp -> { /* don't save these as unknown properties */ }
182
                    else -> e.unknownProperties += prop
Ricki Hirner's avatar
Ricki Hirner committed
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
                }

            // calculate DtEnd from Duration
            if (e.dtEnd == null && e.duration != null)
                e.dtEnd = event.getEndDate(true)

            e.alarms.addAll(event.alarms)

            // validation
            if (e.dtStart == null)
                throw InvalidCalendarException("Event without start time")

            return e
        }
    }

Ricki Hirner's avatar
Ricki Hirner committed
199

Ricki Hirner's avatar
Ricki Hirner committed
200 201
    fun write(os: OutputStream) {
        val ical = Calendar()
202 203
        ical.properties += Version.VERSION_2_0
        ical.properties += prodId
Ricki Hirner's avatar
Ricki Hirner committed
204

205 206 207
        val dtStart = dtStart ?: throw InvalidCalendarException("Won't generate event without start time")

        // "main event" (without exceptions)
Ricki Hirner's avatar
Ricki Hirner committed
208
        val components = ical.components
209 210
        val mainEvent = toVEvent()
        components += mainEvent
Ricki Hirner's avatar
Ricki Hirner committed
211 212 213

        // remember used time zones
        val usedTimeZones = mutableSetOf<TimeZone>()
214
        dtStart.timeZone?.let(usedTimeZones::add)
Ricki Hirner's avatar
Ricki Hirner committed
215 216 217 218
        dtEnd?.timeZone?.let(usedTimeZones::add)

        // recurrence exceptions
        for (exception in exceptions) {
219 220 221 222 223 224 225 226 227
            // make sure that
            //     - exceptions have the same UID as the main event and
            //     - RECURRENCE-IDs have the same timezone as the main event's DTSTART
            exception.uid = uid
            exception.recurrenceId?.let { recurrenceId ->
                if (recurrenceId.timeZone != dtStart.timeZone) {
                    recurrenceId.timeZone = dtStart.timeZone
                    exception.recurrenceId = recurrenceId
                }
Ricki Hirner's avatar
Ricki Hirner committed
228

229 230 231 232 233 234 235 236
                // create 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)
            }
Ricki Hirner's avatar
Ricki Hirner committed
237 238
        }

239
        // add VTIMEZONE components
Ricki Hirner's avatar
Ricki Hirner committed
240 241
        usedTimeZones.forEach {
            val tz = it.vTimeZone
242
            // TODO dtStart?.let { minifyVTimeZone(tz, it.date) }
Ricki Hirner's avatar
Ricki Hirner committed
243 244
            ical.components += tz
        }
Ricki Hirner's avatar
Ricki Hirner committed
245 246 247 248

        CalendarOutputter(false).output(ical, os)
    }

249 250 251 252 253 254
    /**
     * Generates a VEvent representation of this event.
     *
     * @return generated VEvent
     */
    private fun toVEvent(): VEvent {
Ricki Hirner's avatar
Ricki Hirner committed
255 256 257
        val event = VEvent()
        val props = event.properties

258
        props += Uid(uid)
Ricki Hirner's avatar
Ricki Hirner committed
259 260
        recurrenceId?.let { props += it }
        sequence?.let { if (it != 0) props += Sequence(it) }
Ricki Hirner's avatar
Ricki Hirner committed
261

Ricki Hirner's avatar
Ricki Hirner committed
262 263 264
        summary?.let { props += Summary(it) }
        location?.let { props += Location(it) }
        description?.let { props += Description(it) }
Ricki Hirner's avatar
Ricki Hirner committed
265
        color?.let { props += Color(null, it.name) }
Ricki Hirner's avatar
Ricki Hirner committed
266

267
        props += dtStart
Ricki Hirner's avatar
Ricki Hirner committed
268 269
        dtEnd?.let { props += it }
        duration?.let { props += it }
Ricki Hirner's avatar
Ricki Hirner committed
270

Ricki Hirner's avatar
Ricki Hirner committed
271
        rRule?.let { props += it }
Ricki Hirner's avatar
Ricki Hirner committed
272
        props.addAll(rDates)
Ricki Hirner's avatar
Ricki Hirner committed
273
        exRule?.let { props += it }
Ricki Hirner's avatar
Ricki Hirner committed
274 275
        props.addAll(exDates)

276
        classification?.let { props += it }
Ricki Hirner's avatar
Ricki Hirner committed
277
        status?.let { props += it }
Ricki Hirner's avatar
Ricki Hirner committed
278
        if (!opaque)
279
            props += Transp.TRANSPARENT
Ricki Hirner's avatar
Ricki Hirner committed
280

Ricki Hirner's avatar
Ricki Hirner committed
281
        organizer?.let { props += it }
Ricki Hirner's avatar
Ricki Hirner committed
282 283
        props.addAll(attendees)

284
        props.addAll(unknownProperties)
Ricki Hirner's avatar
Ricki Hirner committed
285

Ricki Hirner's avatar
Ricki Hirner committed
286
        lastModified?.let { props += it }
Ricki Hirner's avatar
Ricki Hirner committed
287 288 289 290 291 292

        event.alarms.addAll(alarms)
        return event
    }


293
    // helpers
Ricki Hirner's avatar
Ricki Hirner committed
294 295 296

    fun isAllDay() = !isDateTime(dtStart)

297
}