2021-01-30 22:05:44 +00:00
|
|
|
//
|
|
|
|
// RaPlaFetcher.swift
|
|
|
|
// DHBW-Service
|
|
|
|
//
|
|
|
|
// Created by Patrick Müller on 29.01.21.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
import CoreData
|
|
|
|
|
|
|
|
class RaPlaFetcher {
|
|
|
|
public class iCalEvent {
|
2021-04-07 11:26:37 +00:00
|
|
|
var startDate: Date = Date() // DTSTART
|
|
|
|
var endDate: Date = Date() // DTEND
|
|
|
|
var summary: String = "" // SUMMARY
|
|
|
|
var description: String = "" // DESCRIPTION
|
|
|
|
var location: String = "" // LOCATION
|
|
|
|
var category: String = "" // CATEGORIES
|
|
|
|
var uid: String = "" // UID
|
|
|
|
var lecturers: [LecturerObj] = [] // ATTENDEE
|
|
|
|
var excludedDates: [Date] = [] // EXDATE
|
|
|
|
var isRecurring: Bool = false // If the event is recurring
|
|
|
|
var frequency: String = "" // Frequence in case of recurring events, e.g. DAILY or WEEKLY
|
|
|
|
var recCount: Int = 0 // How often the event occurs in case of recurring events
|
|
|
|
var recInterval: Int = 0 // Interval of the recurring event, e.g. 1 for every week / day / ...
|
|
|
|
var recDay: String = "" // The day of the recurrence, e.g. FR for friday
|
|
|
|
var recUntil: Date = Date() // Until when the event has to be repeated
|
2021-02-10 18:17:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public class LecturerObj {
|
|
|
|
var name: String = ""
|
|
|
|
var email: String = ""
|
2021-01-30 22:05:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Get the RaPla file from the given URL and save the events to CoreData
|
|
|
|
public class func getRaplaFileAndSaveToCoreData(from urlString: String) -> Bool {
|
|
|
|
let file = getFileAsString(from: urlString)
|
|
|
|
|
|
|
|
let eventStrings = splitIntoEvents(file: file)
|
|
|
|
|
|
|
|
let eventObjects = convertStringsToObjects(eventStrings: eventStrings)
|
|
|
|
|
|
|
|
return saveToCoreData(eventObjects: eventObjects)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the RaPla files from the given URL and return the event objects
|
|
|
|
public class func getRaplaFileAndReturnEvents(from urlString: String) -> [iCalEvent] {
|
|
|
|
let file = getFileAsString(from: urlString)
|
|
|
|
|
|
|
|
let eventStrings = splitIntoEvents(file: file)
|
|
|
|
|
|
|
|
return convertStringsToObjects(eventStrings: eventStrings)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GET the file from the given URL and convert it to a String that is then returned
|
|
|
|
private class func getFileAsString(from urlString: String) -> String {
|
|
|
|
let url = URL(string: urlString)!
|
|
|
|
var file: String = ""
|
|
|
|
|
|
|
|
do {
|
|
|
|
file = try String(contentsOf: url, encoding: .utf8)
|
|
|
|
} catch let error {
|
|
|
|
print(error.localizedDescription)
|
|
|
|
}
|
|
|
|
|
|
|
|
return file
|
|
|
|
}
|
|
|
|
|
|
|
|
// Split the given ical file string into individual event strings and return them as a list
|
|
|
|
private class func splitIntoEvents(file: String) -> [String] {
|
|
|
|
let regexOptions: NSRegularExpression.Options = [.dotMatchesLineSeparators]
|
|
|
|
// Regex explanation: Matches BEGIN:VEVENT "Any character" END:VEVENT across multiple lines. The *? assures that we receive the
|
|
|
|
// maximum amount of matches, i.e. it makes the regex non-greedy as we would otherwise just receive one giant match
|
|
|
|
let eventStrings = UtilityFunctions.regexMatches(for: "BEGIN:VEVENT.*?END:VEVENT", with: regexOptions, in: file)
|
|
|
|
|
|
|
|
return eventStrings
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert an ical event String into an iCalEvent Codable object as defined above
|
|
|
|
private class func convertStringsToObjects(eventStrings: [String]) -> [iCalEvent] {
|
|
|
|
var events: [iCalEvent] = []
|
|
|
|
|
|
|
|
for eventString in eventStrings {
|
2021-02-10 18:17:11 +00:00
|
|
|
var lines = eventString.components(separatedBy: .newlines)
|
|
|
|
// Remove all blank lines that somehow are generated by the .components function
|
|
|
|
lines = removeBlankLines(lines: lines)
|
|
|
|
|
2021-01-30 22:05:44 +00:00
|
|
|
let evt = iCalEvent()
|
|
|
|
|
2021-02-10 18:17:11 +00:00
|
|
|
// Iterate over all lines and merge lines that have been split by rapla first
|
|
|
|
for lineNr in 0...lines.count-1 {
|
|
|
|
if(lines[lineNr].hasPrefix(" ")){
|
|
|
|
lines[lineNr] = String(lines[lineNr].dropFirst())
|
2021-04-07 11:26:37 +00:00
|
|
|
|
|
|
|
// If there are more than 2 lines that have to be merged, we need to find out how many lines we have to go up
|
|
|
|
var goUp = 1
|
|
|
|
while(lines[lineNr-goUp] == ""){
|
|
|
|
goUp += 1
|
|
|
|
}
|
|
|
|
|
|
|
|
lines[lineNr-goUp].append(lines[lineNr])
|
2021-02-10 18:17:11 +00:00
|
|
|
lines[lineNr] = ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove all blank lines again as we produced some while merging the lines
|
|
|
|
lines = removeBlankLines(lines: lines)
|
|
|
|
|
2021-01-30 22:05:44 +00:00
|
|
|
for line in lines {
|
|
|
|
var lineWithoutPrefix = line
|
|
|
|
if(!line.contains(":")) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
lineWithoutPrefix.removeSubrange(lineWithoutPrefix.startIndex...lineWithoutPrefix.firstIndex(of: ":")!)
|
|
|
|
|
|
|
|
if(line.hasPrefix("DTSTART")){
|
|
|
|
//Date format: 20181101T080000
|
|
|
|
let dateFormatter = DateFormatter()
|
|
|
|
if(lineWithoutPrefix.contains("Z")){
|
2021-02-10 18:17:11 +00:00
|
|
|
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
|
2021-01-30 22:05:44 +00:00
|
|
|
} else {
|
|
|
|
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss"
|
|
|
|
}
|
|
|
|
let date = dateFormatter.date(from: lineWithoutPrefix)!
|
|
|
|
evt.startDate = date
|
|
|
|
} else if(line.hasPrefix("DTEND")){
|
|
|
|
let dateFormatter = DateFormatter()
|
|
|
|
if(lineWithoutPrefix.contains("Z")){
|
2021-02-10 18:17:11 +00:00
|
|
|
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
|
2021-01-30 22:05:44 +00:00
|
|
|
} else {
|
|
|
|
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss"
|
|
|
|
}
|
|
|
|
let date = dateFormatter.date(from: lineWithoutPrefix)!
|
|
|
|
evt.endDate = date
|
|
|
|
} else if(line.hasPrefix("SUMMARY")){
|
|
|
|
evt.summary = lineWithoutPrefix
|
|
|
|
} else if(line.hasPrefix("DESCRIPTION")){
|
|
|
|
evt.description = lineWithoutPrefix
|
|
|
|
} else if(line.hasPrefix("LOCATION")){
|
|
|
|
evt.location = lineWithoutPrefix
|
|
|
|
} else if(line.hasPrefix("CATEGORIES")){
|
|
|
|
evt.category = lineWithoutPrefix
|
2021-01-31 18:49:52 +00:00
|
|
|
} else if(line.hasPrefix("UID")){
|
|
|
|
evt.uid = lineWithoutPrefix
|
2021-02-10 18:17:11 +00:00
|
|
|
} else if(line.hasPrefix("ATTENDEE;ROLE=REQ-PARTICIPANT;")) {
|
|
|
|
var lecturerName = line
|
|
|
|
|
|
|
|
let begin = lecturerName.firstIndex(of: "\"")!
|
|
|
|
lecturerName.removeSubrange(lecturerName.startIndex...begin)
|
|
|
|
|
|
|
|
let end = lecturerName.lastIndex(of: "\"")!
|
|
|
|
lecturerName = String(lecturerName[..<end])
|
|
|
|
|
|
|
|
let lecturerEmail = String(lineWithoutPrefix[String.Index(utf16Offset: 7, in: lineWithoutPrefix)..<lineWithoutPrefix.endIndex])
|
|
|
|
|
|
|
|
let lecturer = LecturerObj()
|
|
|
|
lecturer.name = lecturerName
|
|
|
|
lecturer.email = lecturerEmail
|
|
|
|
|
|
|
|
evt.lecturers.append(lecturer)
|
2021-04-07 11:26:37 +00:00
|
|
|
} else if(line.hasPrefix("RRULE")) {
|
|
|
|
// This line normally looks like this: RRULE:FREQ=WEEKLY;COUNT=12;INTERVAL=1;BYDAY=TU
|
|
|
|
// Can also look smth like this though: RRULE:FREQ=WEEKLY;COUNT=12
|
|
|
|
|
|
|
|
evt.isRecurring = true
|
|
|
|
|
|
|
|
let params = lineWithoutPrefix.components(separatedBy: ";")
|
|
|
|
|
|
|
|
for param in params {
|
|
|
|
let keyword = param[param.startIndex..<param.firstIndex(of: "=")!]
|
|
|
|
let value = param[param.index(param.firstIndex(of: "=")!, offsetBy: 1)..<param.endIndex]
|
|
|
|
|
|
|
|
switch keyword {
|
|
|
|
case "FREQ":
|
|
|
|
evt.frequency = String(value)
|
|
|
|
break
|
|
|
|
case "COUNT":
|
|
|
|
evt.recCount = Int(value)!
|
|
|
|
break
|
|
|
|
case "INTERVAL":
|
|
|
|
evt.recInterval = Int(value)!
|
|
|
|
break
|
|
|
|
case "BYDAY":
|
|
|
|
evt.recDay = String(value)
|
|
|
|
break
|
|
|
|
case "UNTIL":
|
|
|
|
let dateFormatter = DateFormatter()
|
|
|
|
dateFormatter.dateFormat = "yyyyMMdd"
|
|
|
|
let date = dateFormatter.date(from: String(value))!
|
|
|
|
evt.recUntil = date
|
|
|
|
break
|
|
|
|
default:
|
|
|
|
print("Unknown parameter found: " + line)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if (line.hasPrefix("EXDATE")) {
|
|
|
|
// Excluded from recurring events
|
|
|
|
// Format: 20210401T133000,20210408T133000,20210513T133000
|
|
|
|
|
|
|
|
let exclDateStrings = lineWithoutPrefix.components(separatedBy: ",")
|
|
|
|
|
|
|
|
for exclDate in exclDateStrings {
|
|
|
|
let dateFormatter = DateFormatter()
|
|
|
|
if(lineWithoutPrefix.contains("Z")){
|
|
|
|
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
|
|
|
|
} else {
|
|
|
|
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss"
|
|
|
|
}
|
|
|
|
let date = dateFormatter.date(from: exclDate)!
|
|
|
|
evt.excludedDates.append(date)
|
|
|
|
}
|
2021-01-30 22:05:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
events.append(evt)
|
|
|
|
}
|
|
|
|
|
|
|
|
return events
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save the given iCalEvent objects to CoreData
|
2021-01-31 22:10:06 +00:00
|
|
|
// Updates the events if they already exist and deletes old (/invalid) ones
|
2021-01-30 22:05:44 +00:00
|
|
|
private class func saveToCoreData(eventObjects: [iCalEvent]) -> Bool{
|
2021-04-07 11:26:37 +00:00
|
|
|
// Get known UIDs
|
2021-02-10 21:20:10 +00:00
|
|
|
let existingEvents: [RaPlaEvent] = RaPlaEvent.getAll()
|
2021-02-10 18:17:11 +00:00
|
|
|
var existingEventsDict: [String:RaPlaEvent] = [:]
|
2021-01-31 18:49:52 +00:00
|
|
|
for event in existingEvents {
|
2021-02-10 21:20:10 +00:00
|
|
|
existingEventsDict[event.uid!] = event
|
2021-01-31 18:49:52 +00:00
|
|
|
}
|
2021-04-07 11:26:37 +00:00
|
|
|
// List for new UIDs
|
|
|
|
var newEventUIDs: [String] = []
|
2021-01-31 18:49:52 +00:00
|
|
|
|
|
|
|
for event in eventObjects {
|
|
|
|
// If the event already exists locally, update it. Otherwise, create a new record
|
2021-04-07 11:26:37 +00:00
|
|
|
if(event.isRecurring){
|
|
|
|
// Create as many events as we need
|
|
|
|
// If we e.g. need 12 events, we create 0...11
|
|
|
|
for iteration in 0..<event.recCount {
|
|
|
|
// Calculate start- and enddate
|
|
|
|
// Calculate offset
|
|
|
|
let offsetType: Calendar.Component
|
|
|
|
let offsetAmount: Int
|
|
|
|
|
|
|
|
switch event.frequency {
|
|
|
|
case "DAILY":
|
|
|
|
offsetType = Calendar.Component.day
|
|
|
|
offsetAmount = 1
|
|
|
|
break
|
|
|
|
case "WEEKLY":
|
|
|
|
offsetType = Calendar.Component.day
|
|
|
|
offsetAmount = 7
|
|
|
|
break
|
|
|
|
case "MONTHLY":
|
|
|
|
offsetType = Calendar.Component.month
|
|
|
|
offsetAmount = 1
|
|
|
|
break
|
|
|
|
case "YEARLY":
|
|
|
|
offsetType = Calendar.Component.year
|
|
|
|
offsetAmount = 1
|
|
|
|
break
|
|
|
|
default:
|
|
|
|
offsetType = Calendar.Component.day
|
|
|
|
offsetAmount = 0
|
|
|
|
print("Found unknown frequency: " + event.frequency)
|
|
|
|
}
|
|
|
|
|
|
|
|
let startDate = Calendar.current.date(byAdding: offsetType, value: (offsetAmount * iteration), to: event.startDate)!
|
|
|
|
let endDate = Calendar.current.date(byAdding: offsetType, value: (offsetAmount * iteration), to: event.endDate)!
|
|
|
|
|
|
|
|
// Check if this recurrence should be excluded
|
|
|
|
if(event.excludedDates.contains(startDate)){
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate UID
|
|
|
|
// Appending iteration to distinguish between recurring events
|
|
|
|
let newUID = event.uid + "---" + String(iteration)
|
|
|
|
|
|
|
|
// Create or update existing CoreData object
|
|
|
|
let evt: RaPlaEvent
|
|
|
|
if existingEventsDict.keys.contains(newUID) {
|
|
|
|
evt = existingEventsDict[newUID]!
|
|
|
|
} else {
|
|
|
|
evt = RaPlaEvent(context: PersistenceController.shared.context)
|
|
|
|
|
|
|
|
// Set default values for new object
|
|
|
|
evt.isHidden = false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Populate fields
|
|
|
|
// (offsetAmount * iteration) because for the 1st event, we dont want to add an offset, and
|
|
|
|
// for every event after that we want to add e.g. 1 week, 2 weeks, 3 weeks etc.
|
|
|
|
evt.startDate = startDate
|
|
|
|
evt.endDate = endDate
|
|
|
|
evt.summary = event.summary
|
|
|
|
evt.descr = event.description
|
|
|
|
evt.location = event.location
|
|
|
|
evt.category = event.category
|
|
|
|
evt.uid = newUID
|
|
|
|
for lecturer in event.lecturers {
|
|
|
|
let lect = Lecturer(context: PersistenceController.shared.context)
|
|
|
|
lect.name = lecturer.name
|
|
|
|
lect.email = lecturer.email
|
|
|
|
lect.event = evt
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add UID to new UIDs list
|
|
|
|
newEventUIDs.append(newUID)
|
|
|
|
}
|
2021-01-31 18:49:52 +00:00
|
|
|
} else {
|
2021-04-07 11:26:37 +00:00
|
|
|
// Generate UID
|
|
|
|
let newUID = event.uid + "---0" // Appending ---0 to distinguish between recurring events
|
2021-02-01 22:17:15 +00:00
|
|
|
|
2021-04-07 11:26:37 +00:00
|
|
|
// Create or update existing CoreData object
|
|
|
|
let evt: RaPlaEvent
|
|
|
|
if existingEventsDict.keys.contains(newUID) {
|
|
|
|
evt = existingEventsDict[newUID]!
|
|
|
|
} else {
|
|
|
|
evt = RaPlaEvent(context: PersistenceController.shared.context)
|
|
|
|
|
|
|
|
// Set default values for new object
|
|
|
|
evt.isHidden = false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Populate fields
|
|
|
|
evt.startDate = event.startDate
|
|
|
|
evt.endDate = event.endDate
|
|
|
|
evt.summary = event.summary
|
|
|
|
evt.descr = event.description
|
|
|
|
evt.location = event.location
|
|
|
|
evt.category = event.category
|
|
|
|
evt.uid = newUID
|
|
|
|
for lecturer in event.lecturers {
|
|
|
|
let lect = Lecturer(context: PersistenceController.shared.context)
|
|
|
|
lect.name = lecturer.name
|
|
|
|
lect.email = lecturer.email
|
|
|
|
lect.event = evt
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add UID to new UIDs list
|
|
|
|
newEventUIDs.append(newUID)
|
2021-01-30 22:05:44 +00:00
|
|
|
}
|
|
|
|
}
|
2021-01-31 18:49:52 +00:00
|
|
|
|
2021-01-31 22:10:06 +00:00
|
|
|
// Now we also have to delete locally stored events that have been deleted from RaPla
|
|
|
|
for localUid in existingEventsDict.keys {
|
|
|
|
if(!newEventUIDs.contains(localUid)){
|
|
|
|
// Locally stored event does not exist in RaPla anymore, delete it
|
|
|
|
let evt = existingEventsDict[localUid]
|
|
|
|
PersistenceController.shared.context.delete(evt!)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-31 18:49:52 +00:00
|
|
|
PersistenceController.shared.save()
|
|
|
|
|
|
|
|
return true
|
2021-01-30 22:05:44 +00:00
|
|
|
}
|
2021-02-10 18:17:11 +00:00
|
|
|
|
|
|
|
private class func removeBlankLines(lines: [String]) -> [String] {
|
|
|
|
var newLines = lines
|
|
|
|
|
|
|
|
// Remove all blank lines that somehow are generated by the .components function
|
|
|
|
for line in newLines {
|
|
|
|
if(line.isEmpty){
|
|
|
|
newLines.remove(at: newLines.firstIndex(of: line)!)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return newLines
|
|
|
|
}
|
2021-01-30 22:05:44 +00:00
|
|
|
}
|