// Copyright (C) 2016 Inigo Valentin
//
// This file is part of the Gasteizko Margolariak IOS app.
//
// The Gasteizko Margolariak IOS app is free software: you can
// redistribute it and/or modify it under the terms of the
// GNU General Public License as published by the Free Software
// Foundation, either version 3 of the License, or (at your
// option) any later version.
//
// The Gasteizko Margolariak IOS app is distributed in the
// hope that it will be useful, but WITHOUT ANY WARRANTY;
// without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
// Public License for more details.
//
// You should have received a copy of the GNU General Public
// License along with the Gasteizko Margolariak IOS app.
// If not, see .
import Foundation
import CoreData
import UIKit
/**
Class to handle server sync.
*/
class Sync{
var initial: Bool = false
/**
Starts the sync process.
Always asynchronously.
*/
init(){
let url = buildUrl();
sync(url: url)
}
/**
Starts the sync process, synchronously or asynchronously.
:param: synchronous True for synchronous sync, false for asynchronously
*/
init(synchronous: Bool){
if synchronous == true{
NSLog(":SYNC:LOG: Synchronous sync started.")
self.initial = true
}
let url = buildUrl();
sync(url: url)
}
/**
Builds the URL to perform the sync against.
:returns: The URL.
*/
func buildUrl() -> URL{
// Get user ID
let defaults = UserDefaults.standard
var uId: String = "unknown"
if defaults.value(forKey: "userId") != nil{
uId = defaults.value(forKey: "userId") as! String
}
// Get versions
var strVersions = ""
do{
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
let appDelegate = UIApplication.shared.delegate as! AppDelegate
context.persistentStoreCoordinator = appDelegate.persistentStoreCoordinator
let fetchRequest: NSFetchRequest = Version.fetchRequest()
let searchResults = try context.fetch(fetchRequest)
var s: String = ""
var v: Int = 0
for r in searchResults {
s = r.value(forKey: "section")! as! String
v = r.value(forKey: "version")! as! Int
strVersions = strVersions + "&\(s)=\(v)"
}
}
catch let error as NSError{
NSLog(":SYNC:ERROR: Error getting stored versions: \(error)")
}
// Build URL
// TODO: Change host for production
var urlStr: String = ""
if initial{
urlStr = "http://192.168.1.101/API/v3/fastsync.php?client=com.margolariak.app&user=\(uId)\(strVersions)"
}
else{
urlStr = "http://192.168.1.101/API/v3/sync.php?client=com.margolariak.app&user=\(uId)\(strVersions)"
}
let url = URL(string: urlStr)
NSLog(":SYNC:LOG: URL built: \(String(describing: url))")
return url!
}
/**
Performs an asynchronous sync.
It fetches the info from the server and stores as Core Data
:param: url The url to sync.
*/
func sync(url: URL){
NSLog(":SYNC:LOG: Sync started.")
let delegate: AppDelegate = UIApplication.shared.delegate as! AppDelegate
delegate.syncController?.nowSyncing = true
//Synchronously get data
let task = URLSession.shared.dataTask(with: url) { dat, response, error in
guard error == nil else {
NSLog(":SYNC:ERROR: Unknown error.")
return
}
guard let rawData = dat else {
NSLog(":SYNC:ERROR: Data is empty.")
return
}
var data = String(data:rawData, encoding: String.Encoding.utf8)
NSLog(":SYNC:LOG: Data received.")
// Loop tables and save them to core data
var table: String;
var content: String;
while (data?.indexOf(target: "}]") != nil){
table = data!.subStr(start: 3, end: data!.indexOf(target: "\":")! - 1)
content = data!.subStr(start: table.length + 5, end: (data?.indexOf(target: "}]"))! + 1)
if table == "settings"{ // Special case
self.saveSettings(content: content)
}
else if table == "version"{ // Special case
self.saveVersion(content: content)
}
else{
self.saveTable(table: table, content: content)
}
data = data?.subStr(start: (data?.indexOf(target: "}]"))! + 1, end: (data?.length)! - 1)
}
// If it's the initial sync, hide the segue
if self.initial == true{
NSLog(":SYNC:LOG: Finishing synchronous sync.")
let delegate: AppDelegate = UIApplication.shared.delegate as! AppDelegate
delegate.syncController?.nowSyncing = false
}
}
task.resume()
}
/**
Saves the data in the table.
:param: table Name of the table
:param: content JSON string containing the rows of the table.
*/
func saveTable(table: String, content: String){
NSLog(":SYNC:LOG: Saving table \(table)")
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dateFormatter.timeZone = TimeZone.ReferenceType.local
//Set up context
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.persistentStoreCoordinator = appDelegate.persistentStoreCoordinator
let entity = NSEntityDescription.entity(forEntityName: table.capitalize(), in: context)
//Delete all previous entries
let fetch = NSFetchRequest(entityName: table.capitalize())
let request = NSBatchDeleteRequest(fetchRequest: fetch)
do {
try context.execute(request)
} catch let error as NSError {
NSLog(":SYNC:ERROR: Could not clean up \(table.capitalize()) entity: \(error), \(error.userInfo).")
} catch {
NSLog(":SYNC:ERROR: Could not clean up \(table.capitalize()) entity.")
}
//Loop rows
var data: String = content
var row: String
var column: String
var value: String
var tuple: String
var query: NSManagedObject
while data.indexOf(target: "}") != nil{
row = data.subStr(start: data.indexOf(target: "{")! + 1, end: data.indexOf(target: "}")! - 1)
row = "\(row),\""
query = NSManagedObject(entity: entity!, insertInto: context)
while row.indexOf(target: ",\"") != nil{
tuple = row.subStr(start: 0, end: row.indexOf(target: ",\"")! - 1)
column = tuple.subStr(start: tuple.indexOf(target: "\"")! + 1, end: tuple.indexOf(target: "\":")! - 1)
if column.length + 4 >= tuple.length - 2{
value = "ul"
}
else{
value = tuple.subStr(start: column.length + 4, end: tuple.length - 2)
}
if value != "ul"{ // 'ul' rom 'null' or empty. If it is, just do nothing.
if entity?.attributesByName[column]?.attributeType == .stringAttributeType{
query.setValue(value, forKey: column)
}
else if entity?.attributesByName[column]?.attributeType == .integer16AttributeType || entity?.attributesByName[column]?.attributeType == .integer32AttributeType || entity?.attributesByName[column]?.attributeType == .integer64AttributeType{
query.setValue(Int(value), forKey: column)
}
else if entity?.attributesByName[column]?.attributeType == .booleanAttributeType{
if Int(value) == 1{
query.setValue(true, forKey: column)
}
else if Int(value) == 0{
query.setValue(false, forKey: column)
}
}
else if entity?.attributesByName[column]?.attributeType == .dateAttributeType{
if value.length < 11{
value = "\(value) 00:00:00"
}
query.setValue(dateFormatter.date(from: value)!, forKey: column)
}
else{ //Regular string
}
// Special case: "festivl_event_citiy" and "festival_event_gm" have a special column: "day"
if (table == "festival_event_city" || table == "festival_event_gm") && column == "start" {
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dateFormatter.calendar = NSCalendar(calendarIdentifier: NSCalendar.Identifier.ISO8601)! as Calendar
dateFormatter.locale = NSLocale(localeIdentifier: "en_US_POSIX") as Locale!
dateFormatter.timeZone = NSTimeZone.local
let timeString = value//"\(value.subStr(start: 0, end: 19))"
let dayString = "\(value.subStr(start: 0, end: 10)) 00:00:00"
var day = dateFormatter.date(from: dayString)!
let start = dateFormatter.date(from: timeString)!
// If on the first hours of the next day...
let calendar = Calendar.current
let hours = calendar.component(.hour, from: start )
// ... the event belongs to the previous day.
if hours < 6{
day = Calendar.current.date(byAdding: .day, value: -1, to: day)!
}
query.setValue(day, forKey: "day")
}
}
let start: Int = row.indexOf(target: ",\"")! + 2
let end: Int = row.length
if (start == end){
row = ""
}
else{
row = row.subStr(start: row.indexOf(target: ",\"")! + 1, end: row.length - 1)
}
}
do{
// Save the row
try context.save()
}
catch let error as NSError {
NSLog(":SYNC:ERROR: Could not save a row for table \(table.capitalize()): \(error), \(error.userInfo).")
}
data = data.subStr(start: data.indexOf(target: "}")! + 1, end: data.length - 1)
}
}
/**
Saves the received settings.
:param: content JSON string containing the rows of the table.
*/
func saveSettings(content: String){
NSLog(":SYNC:LOG: Saving settings")
var row: String
var tuple: String
var column: String
var value: String
var name: String = "dummy"
let defaults = UserDefaults.standard
//Loop rows
var data: String = content
while (data.indexOf(target: "}") != nil){
row = data.subStr(start: data.indexOf(target: "{")! + 1, end: data.indexOf(target: "}")! - 1)
row = "\(row),\""
while (row.indexOf(target: ",\"") != nil){
tuple = row.subStr(start: 0, end: row.indexOf(target: ",\"")! - 1)
column = tuple.subStr(start: 1, end: tuple.indexOf(target: "\":")! - 1)
value = tuple.subStr(start: column.length + 4, end: tuple.length - 2)
if (column == "name"){
name = value
}
else if (column == "value"){
defaults.set(value, forKey: name)
}
let start: Int = row.indexOf(target: ",\"")! + 1
let end: Int = row.length - 1
if (start == end){
row = ""
}
else{
row = row.subStr(start: start, end: end)
}
}
data = data.subStr(start: data.indexOf(target: "}")! + 1, end: data.length - 1)
}
}
/**
Saves the new versions of the tables
:param: content: JSON string.
*/
func saveVersion(content: String){
NSLog(":SYNC:LOG: Saving versions")
var data: String = content
var row: String
var section: String = "dummy"
var version: String = "0"
var column: String
var tuple: String
var value: String
//Set up context
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.persistentStoreCoordinator = appDelegate.persistentStoreCoordinator
let entity = NSEntityDescription.entity(forEntityName: "Version", in: context)
//Loop rows
var query: NSManagedObject
while (data.indexOf(target: "}") != nil){
row = data.subStr(start: data.indexOf(target: "{")! + 1, end: data.indexOf(target: "}")! - 1)
row = "\(row),\""
while (row.indexOf(target: ",\"") != nil){
tuple = row.subStr(start: 0, end: row.indexOf(target: ",\"")! - 1)
column = tuple.subStr(start: 1, end: tuple.indexOf(target: "\":")! - 1)
value = tuple.subStr(start: column.length + 4, end: tuple.length - 2)
if column == "section"{
section = value
}
else if column == "version"{
version = value
// Check if version is already in database.
context.persistentStoreCoordinator = appDelegate.persistentStoreCoordinator
let fetchRequest: NSFetchRequest = Version.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "section = %@", section)
do {
let searchResults = try context.fetch(fetchRequest)
if searchResults.count > 0{
let v = searchResults[0]
v.setValue(Int(version), forKey: "version")
}
else{
query = NSManagedObject(entity: entity!, insertInto: context)
query.setValue(section, forKey: "section")
query.setValue(Int(version), forKey: "version")
}
// Save the version
try context.save()
}
catch let error as NSError {
NSLog(":SYNC:ERROR: Could not store a version: \(error), \(error.userInfo).")
}
catch {
NSLog(":SYNC:ERROR: Could not store a version.")
}
}
let start: Int = row.indexOf(target: ",\"")! + 2
let end: Int = row.length
if (start == end){
row = ""
}
else{
row = row.subStr(start: row.indexOf(target: ",\"")! + 1, end: row.length - 1)
}
}
data = data.subStr(start: data.indexOf(target: "}")! + 1, end: data.length - 1)
}
}
}