DHBW-Service-App/DHBW-Service/Utility/RaPlaFetcher.swift

374 lines
16 KiB
Swift

//
// RaPlaFetcher.swift
// DHBW-Service
//
// Created by Patrick Müller on 29.01.21.
//
import Foundation
import CoreData
class RaPlaFetcher {
public class iCalEvent {
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
}
public class LecturerObj {
var name: String = ""
var email: String = ""
}
// 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 {
var lines = eventString.components(separatedBy: .newlines)
// Remove all blank lines that somehow are generated by the .components function
lines = removeBlankLines(lines: lines)
let evt = iCalEvent()
// 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())
// 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])
lines[lineNr] = ""
}
}
// Remove all blank lines again as we produced some while merging the lines
lines = removeBlankLines(lines: lines)
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")){
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
} 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")){
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
} 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
} else if(line.hasPrefix("UID")){
evt.uid = lineWithoutPrefix
} 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)
} 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)
}
}
}
events.append(evt)
}
return events
}
// Save the given iCalEvent objects to CoreData
// Updates the events if they already exist and deletes old (/invalid) ones
private class func saveToCoreData(eventObjects: [iCalEvent]) -> Bool{
// Get known UIDs
let existingEvents: [RaPlaEvent] = RaPlaEvent.getAll()
var existingEventsDict: [String:RaPlaEvent] = [:]
for event in existingEvents {
existingEventsDict[event.uid!] = event
}
// List for new UIDs
var newEventUIDs: [String] = []
for event in eventObjects {
// If the event already exists locally, update it. Otherwise, create a new record
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)
}
// (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.
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
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 {
// TODO: Delete all old lecturer objects
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)
}
} else {
// Generate UID
let newUID = event.uid + "---0" // Appending ---0 to distinguish between recurring events
// 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)
}
}
// 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!)
}
}
PersistenceController.shared.save()
return true
}
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
}
}