Task.kt 6.71 KB
Newer Older
Ricki Hirner's avatar
Ricki Hirner committed
1
/*
Ricki Hirner's avatar
Ricki Hirner committed
2
 * Copyright © Ricki Hirner (bitfire web engineering).
Ricki Hirner's avatar
Ricki Hirner committed
3 4 5 6 7 8 9 10
 * 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
 */

package at.bitfire.ical4android

11 12 13 14 15 16 17 18 19 20
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.DateTime
import net.fortuna.ical4j.model.TimeZone
import net.fortuna.ical4j.model.component.VToDo
import net.fortuna.ical4j.model.property.*
import java.io.IOException
import java.io.OutputStream
21
import java.io.Reader
22 23 24 25
import java.net.URI
import java.net.URISyntaxException
import java.util.*
import java.util.logging.Level
Ricki Hirner's avatar
Ricki Hirner committed
26 27 28 29 30 31 32 33 34

class Task: iCalendar() {

	var createdAt: Long? = null
    var lastModified: Long? = null

    var summary: String? = null
    var location: String? = null
    var description: String? = null
Ricki Hirner's avatar
Ricki Hirner committed
35
    var color: Int? = null
Ricki Hirner's avatar
Ricki Hirner committed
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
    var url: String? = null
    var organizer: Organizer? = null
    var geoPosition: Geo? = null
    var priority: Int = Priority.UNDEFINED.level
    var classification: Clazz? = null
    var status: Status? = null

    var dtStart: DtStart? = null
    var due: Due? = null
    var duration: Duration? = null
    var completedAt: Completed? = null
    var percentComplete: Int? = null

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

    companion object {

        /**
         * Parses an InputStream that contains iCalendar VTODOs.
         *
58
         * @param reader  reader for the input stream containing the VTODOs (pay attention to the charset)
Ricki Hirner's avatar
Ricki Hirner committed
59 60 61 62 63 64
         * @return array of filled Task data objects (may have size 0) – doesn't return null
         * @throws IOException
         * @throws InvalidCalendarException on parser exceptions
         */
        @JvmStatic
        @Throws(IOException::class, InvalidCalendarException::class)
65
        fun fromReader(reader: Reader): List<Task>
Ricki Hirner's avatar
Ricki Hirner committed
66
        {
67
            val ical: Calendar
Ricki Hirner's avatar
Ricki Hirner committed
68
            try {
69
                ical = calendarBuilder().build(reader)
Ricki Hirner's avatar
Ricki Hirner committed
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
            } catch (e: ParserException) {
                throw InvalidCalendarException("Couldn't parse iCalendar resource", e)
            }

            val vToDos = ical.getComponents<VToDo>(Component.VTODO)
            return vToDos.mapTo(LinkedList<Task>(), this::fromVToDo)
        }

        @Throws(InvalidCalendarException::class)
        private fun fromVToDo(todo: VToDo): Task {
            val t = Task()

            if (todo.uid != null)
                t.uid = todo.uid.value
            else {
                Constants.log.warning("Received VTODO without UID, generating new one")
                t.generateUID()
            }

            // sequence must only be null for locally created, not-yet-synchronized events
            t.sequence = 0

            for (prop in todo.properties)
                when (prop) {
                    is Sequence -> t.sequence = prop.sequenceNo
                    is Created -> t.createdAt = prop.dateTime.time
                    is LastModified -> t.lastModified = prop.dateTime.time
                    is Summary -> t.summary = prop.value
                    is Location -> t.location = prop.value
                    is Geo -> t.geoPosition = prop
                    is Description -> t.description = prop.value
Ricki Hirner's avatar
Ricki Hirner committed
101
                    is Color -> t.color = prop.value?.rgba
Ricki Hirner's avatar
Ricki Hirner committed
102 103 104 105 106
                    is Url -> t.url = prop.value
                    is Organizer -> t.organizer = prop
                    is Priority -> t.priority = prop.level
                    is Clazz -> t.classification = prop
                    is Status -> t.status = prop
107
                    is Due -> { t.due = prop }
Ricki Hirner's avatar
Ricki Hirner committed
108
                    is Duration -> t.duration = prop
109 110
                    is DtStart -> { t.dtStart = prop }
                    is Completed -> { t.completedAt = prop }
Ricki Hirner's avatar
Ricki Hirner committed
111 112
                    is PercentComplete -> t.percentComplete = prop.percentage
                    is RRule -> t.rRule = prop
113 114
                    is RDate -> t.rDates += prop
                    is ExDate -> t.exDates += prop
Ricki Hirner's avatar
Ricki Hirner committed
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
                }

            // 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
            if (dtStart != null && due != null && !due.date.after(dtStart.date)) {
                Constants.log.warning("Invalid DTSTART >= DUE; ignoring DTSTART")
                t.dtStart = null
            }

            return t
        }

    }


    @Throws(IOException::class)
	fun write(os: OutputStream) {
Ricki Hirner's avatar
Ricki Hirner committed
134
		val ical = Calendar()
135 136
		ical.properties += Version.VERSION_2_0
		ical.properties += prodId
Ricki Hirner's avatar
Ricki Hirner committed
137 138

		val todo = VToDo()
139
		ical.components += todo
Ricki Hirner's avatar
Ricki Hirner committed
140 141
		val props = todo.properties

142
        uid?.let { props += Uid(it) }
Ricki Hirner's avatar
Ricki Hirner committed
143
        sequence?.let { if (it != 0) props += Sequence(sequence as Int) }
Ricki Hirner's avatar
Ricki Hirner committed
144

145 146
		createdAt?.let { props += Created(DateTime(it)) }
        lastModified?.let { props += LastModified(DateTime(it)) }
Ricki Hirner's avatar
Ricki Hirner committed
147

148 149
        summary?.let { props += Summary(it) }
        location?.let { props += Location(it) }
Ricki Hirner's avatar
Ricki Hirner committed
150
        geoPosition?.let { props += it }
151
        description?.let { props += Description(it) }
Ricki Hirner's avatar
Ricki Hirner committed
152
        color?.let { props += Color(EventColor.nearestMatch(it)) }
Ricki Hirner's avatar
Ricki Hirner committed
153 154
		url?.let {
            try {
155
                props += Url(URI(it))
Ricki Hirner's avatar
Ricki Hirner committed
156 157 158 159
            } catch (e: URISyntaxException) {
                Constants.log.log(Level.WARNING, "Ignoring invalid task URL: $url", e)
            }
        }
Ricki Hirner's avatar
Ricki Hirner committed
160
        organizer?.let { props += it }
Ricki Hirner's avatar
Ricki Hirner committed
161

Ricki Hirner's avatar
Ricki Hirner committed
162
        if (priority != Priority.UNDEFINED.level)
163
			props += Priority(priority)
Ricki Hirner's avatar
Ricki Hirner committed
164 165
        classification?.let { props += it }
        status?.let { props += it }
Ricki Hirner's avatar
Ricki Hirner committed
166

Ricki Hirner's avatar
Ricki Hirner committed
167
        rRule?.let { props += it }
168 169
        rDates.forEach { props += it }
        exDates.forEach { props += it }
Ricki Hirner's avatar
Ricki Hirner committed
170 171 172 173

		// remember used time zones
		val usedTimeZones = HashSet<TimeZone>()
		due?.let {
174
            props += it
Ricki Hirner's avatar
Ricki Hirner committed
175 176 177 178
            it.timeZone?.let(usedTimeZones::add)
		}
        duration?.let(props::add)
        dtStart?.let {
179
            props += it
Ricki Hirner's avatar
Ricki Hirner committed
180 181 182
            it.timeZone?.let(usedTimeZones::add)
        }
        completedAt?.let {
183
            props += it
Ricki Hirner's avatar
Ricki Hirner committed
184 185
            it.timeZone?.let(usedTimeZones::add)
        }
186
		percentComplete?.let { props += PercentComplete(it) }
Ricki Hirner's avatar
Ricki Hirner committed
187 188

		// add VTIMEZONE components
189
        usedTimeZones.forEach { ical.components += it.vTimeZone }
Ricki Hirner's avatar
Ricki Hirner committed
190 191 192 193 194 195 196 197 198 199 200 201 202

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


    fun isAllDay(): Boolean {
        val dtStart = dtStart
        val due = due
        return (dtStart != null && !(dtStart.date is DateTime)) ||
               (due != null && !(due.date is DateTime))
    }

}